Skip to content

Fix Content-Length piping RFC violation + optimize chunked mode syscalls#107

Merged
VikramAditya33 merged 1 commit into
mainfrom
fix/test-stream
May 9, 2026
Merged

Fix Content-Length piping RFC violation + optimize chunked mode syscalls#107
VikramAditya33 merged 1 commit into
mainfrom
fix/test-stream

Conversation

@VikramAditya33
Copy link
Copy Markdown
Collaborator

@VikramAditya33 VikramAditya33 commented May 9, 2026

  1. HTTP/1.1 RFC violation: Content-Length header conflicted with chunked transfer encoding during readable.pipe(res)
    When res.setHeader('content-length', size) was set before piping a readable stream, _write() unconditionally used uWS.write() (chunked mode). Per RFC 7230 Section 3.3.2, Transfer-Encoding: chunked cannot coexist with Content-Length, causing HTTPParserError on the client. Fixes readable.pipe(res) with Content-Length header causes HTTP/1.1 protocol violation #104
  • Added contentLengthTotal field to track the declared size
  • setHeader()/removeHeader() now detect and manage this field
  • _write() routes to tryEnd(totalSize) when contentLengthTotal is defined
  • writeHead() skips writing the manual content-length header in tryEnd mode (avoids duplicate)
  • _final() uses uwsRes.end() directly instead of send() for Content-Length mode
  1. Chunked mode: _write() bypassed existing batching, causing excessive syscalls_write() called streamChunk() per chunk, sending each to uWS individually. For fs.createReadStream() with 4KB chunks, this resulted in ~128 uWS.write() syscalls per 512KB instead of ~4 batched writes. Fixes _write() bypasses chunk batching in chunked mode #105
  • Chunked mode now routes _write() through writeChunk() which accumulates chunks in pendingChunks[]
  • Flushes on HIGH_WATERMARK (128KB) or FLUSH_INTERVAL (50ms)
  • Content-Length mode continues using streamChunk() with tryEnd() unchanged

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced HTTP response handling for improved content-length header management and response finalization behavior.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab0a526f-9d52-4b65-8124-83754a982154

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/test-stream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

uWestJS Benchmark Results

Scenario Express req/s Fastify req/s uWestJS req/s Express throughput Fastify throughput uWestJS throughput uWestJS vs Express uWestJS vs Fastify
compression 6.25k 5.26k 7.77k 3.28 MB/s 2.69 MB/s 3.57 MB/s 1.24x 1.48x
headers 23.22k 34.00k 77.83k 4.32 MB/s 6.36 MB/s 12.25 MB/s 3.35x 2.29x
hello-world 24.03k 37.44k 92.45k 3.99 MB/s 6.28 MB/s 9.17 MB/s 3.85x 2.47x
json-response 21.36k 34.82k 67.10k 5.83 MB/s 9.53 MB/s 16.38 MB/s 3.14x 1.93x
mixed-response 21.80k 34.19k 64.11k 5.03 MB/s 7.92 MB/s 12.96 MB/s 2.94x 1.88x
post-json 20.62k 18.17k 32.77k 3.81 MB/s 4.96 MB/s 5.56 MB/s 1.59x 1.80x
query-params 18.93k 32.72k 72.45k 4.17 MB/s 7.24 MB/s 9.74 MB/s 3.83x 2.21x
route-params 22.18k 35.48k 64.28k 5.06 MB/s 8.12 MB/s 12.81 MB/s 2.90x 1.81x
static-file 26.76k 32.51k 68.14k 265.61 MB/s 322.18 MB/s 674.28 MB/s 2.55x 2.10x
streaming-upload 200.01 193.22 197.85 44.53 KB/s 40.38 KB/s 38.26 KB/s 0.99x 1.02x
streaming-with-content-length 752.95 733.27 755.32 3.68 GB/s 3.58 GB/s 3.69 GB/s 1.00x 1.03x
streaming-without-content-length 638.32 649.05 758.66 3.12 GB/s 3.17 GB/s 3.71 GB/s 1.19x 1.17x

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/http/core/response.ts (1)

197-213: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Backpressure is dropped in the chunked-mode _write() path, risking unbounded pendingChunks[] growth.

writeChunk() returns false when flushChunks() hits uWS backpressure, but the chunked branch ignores that return value and unconditionally invokes callback(). The Writable above will keep handing more chunks down, each appended to pendingChunks[], while flushChunks() keeps failing until onWritable drains. Under a slow consumer this trades the old per-chunk syscall pressure for unbounded JS memory growth.

Defer the Writable callback until backpressure clears (or batch within writeChunk itself).

♻️ Suggested approach
     } else {
-      // Chunked mode: use writeChunk() batching for fewer syscalls
-      this.writeChunk(chunk, encoding);
-      callback();
+      // Chunked mode: use writeChunk() batching for fewer syscalls
+      const ok = this.writeChunk(chunk, encoding);
+      if (ok || this.finished || this.aborted) {
+        callback();
+      } else {
+        // Backpressure: wait for drain before signaling Writable readiness
+        this.uwsRes.onWritable(() => {
+          callback();
+          return true;
+        });
+      }
     }

Note: the onWritable handler registered here will race with the one writeChunk schedules for flushChunks(). Consider unifying drain handling so the callback is only resolved after pending chunks actually flush.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/http/core/response.ts` around lines 197 - 213, The chunked branch of
_write currently ignores writeChunk()'s boolean return and calls callback()
immediately, which drops backpressure and allows unbounded growth of
pendingChunks; change _write to check the return of writeChunk(chunk, encoding)
and only call callback() when writeChunk returns true (or when backpressure
clears), and if writeChunk returns false enqueue the callback (e.g.,
pendingWriteCallbacks) to be invoked from flushChunks() or the onWritable
handler once pendingChunks have actually been flushed; unify the drain handling
so both the writeChunk-scheduled flush and the onWritable handler resolve the
same pending callback queue instead of racing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/http/core/response.ts`:
- Around line 342-349: The code that updates this.contentLengthTotal from the
Content-Length header (check involving lowerName === 'content-length') should
strictly validate the header value and clear the stored total on invalid input:
instead of using parseInt (which tolerates trailing junk) accept only a pure
non-negative integer string (e.g. /^\d+$/) derived from the last element when
value is an array, and if it fails validation set this.contentLengthTotal to
undefined/null (not leave the previous valid total); ensure this behavior is
applied wherever headers are normalized/overwritten (affecting writeHead() and
tryEnd() flows) so stale totals cannot survive an invalid overwrite.
- Around line 259-274: The _final() path that currently calls uwsRes.end() when
contentLengthTotal !== undefined must detect incomplete streams (i.e., when
tryEnd()/read tracking did not set this.finished because bytes <
this.contentLengthTotal) and not silently end; instead, if bytes are short,
invoke the Writable final callback with an Error (callback(new Error('incomplete
content-length'))) and ensure the underlying connection is torn down (e.g., call
this.uwsRes.close() or otherwise destroy the response) so the client receives a
connection error rather than a truncated response; update the _final()
implementation (referencing _final(), tryEnd(), this.contentLengthTotal,
this.finished, this.aborted, this.uwsRes.end(), this.uwsRes.close(), and
this.pendingFinalCallback) to perform these checks and call the
pendingFinalCallback/error path when incomplete instead of unconditionally
calling uwsRes.end().

---

Outside diff comments:
In `@src/http/core/response.ts`:
- Around line 197-213: The chunked branch of _write currently ignores
writeChunk()'s boolean return and calls callback() immediately, which drops
backpressure and allows unbounded growth of pendingChunks; change _write to
check the return of writeChunk(chunk, encoding) and only call callback() when
writeChunk returns true (or when backpressure clears), and if writeChunk returns
false enqueue the callback (e.g., pendingWriteCallbacks) to be invoked from
flushChunks() or the onWritable handler once pendingChunks have actually been
flushed; unify the drain handling so both the writeChunk-scheduled flush and the
onWritable handler resolve the same pending callback queue instead of racing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 037f7b72-5250-4cfc-93ac-2a623bc506ef

📥 Commits

Reviewing files that changed from the base of the PR and between 380d261 and 4ec422b.

📒 Files selected for processing (1)
  • src/http/core/response.ts

Comment thread src/http/core/response.ts
Comment thread src/http/core/response.ts
…otocol violation & _write() bypasses chunk batching in chunked mode
@VikramAditya33 VikramAditya33 merged commit b7b1fa9 into main May 9, 2026
4 checks passed
@VikramAditya33 VikramAditya33 deleted the fix/test-stream branch May 9, 2026 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_write() bypasses chunk batching in chunked mode readable.pipe(res) with Content-Length header causes HTTP/1.1 protocol violation

1 participant