Conversation
The ServerResponse header renderer pushed Transfer-Encoding: chunked for
HTTP/1.0 requests while the uWS body writer never chunk-frames an HTTP/1.0
(fromAncientRequest) response, so the head and body disagreed. curl --http1.0
died with 'curl: (56) chunk hex-length char not a hex digit'.
Two cases:
- removeHeader('Content-Length') fell through to forceChunked without
consulting useChunkedEncodingByDefault (false on HTTP/1.0). Node close-
delimits here; now we do too.
- An explicit Transfer-Encoding: chunked header was written to the wire but
the uWS writer's fromAncientRequest gate suppressed chunk framing. Clear
that gate when the user's TE value names 'chunked' so the body matches
the head (like Node). Other TE values (gzip) stay raw.
|
Updated 5:52 PM PT - Jul 9th, 2026
❌ @robobun, your commit 677db27 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 33872That installs a local version of the PR into your bun-33872 --bun |
WalkthroughChangesHTTP/1.0 framing
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
Linked #19789: NGINX's |
There was a problem hiding this comment.
No bugs found — the fix looks correct and fromAncientRequest is only a framing gate (connection-close is set independently), but this is wire-level HTTP framing across the JS layer and C++ uWS bindings, so worth a human sanity check.
Extended reasoning...
Overview
This PR fixes HTTP/1.0 response framing in node:http where the advertised Transfer-Encoding header and the emitted body bytes disagreed. It touches three files: a ~10-line branch in renderNativeHeaders (src/js/node/_http_server.ts) to close-delimit instead of force-chunk when useChunkedEncodingByDefault === false; a new transferEncodingHasChunked helper in src/jsc/bindings/NodeHTTP.cpp (mirroring the existing connectionValueHasClose right above it) that clears httpResponseData->fromAncientRequest when the user explicitly writes a Transfer-Encoding: chunked header; and five new tests asserting exact raw head/body bytes over a raw HTTP/1.0 socket.
Security risks
HTTP response framing is response-smuggling territory. I verified in packages/bun-uws/src/HttpContext.h that HTTP_CONNECTION_CLOSE is set from httpRequest->isAncient() before and independently of fromAncientRequest, and grepped all uses of fromAncientRequest in HttpResponse.h — it gates only chunk-framing paths. So clearing it does not affect connection reuse (the socket still closes after the response), and the change strictly makes the body match a header the user already committed to the wire. I don't see a security regression, but wire-framing changes deserve a second pair of eyes.
Level of scrutiny
Medium-high. The diff is small (~30 production lines), follows existing local patterns exactly, and has strong byte-exact test coverage including a negative guard (Transfer-Encoding: gzip stays identity). But it modifies protocol-level body framing in both the built-in JS module and the C++ uWS binding, which is a critical code path for every node:http server response.
Other factors
No CODEOWNERS cover these files. No prior review comments. The PR description is thorough with a repro, root-cause analysis referencing Node's _storeHeader, and verification against the released binary. The bug-hunting pass found nothing. I'm deferring only because protocol-framing changes across native and JS layers are the kind of thing a maintainer familiar with the uWS integration should confirm.
There was a problem hiding this comment.
I didn't find any bugs, but this touches HTTP wire-framing across two layers (the JS renderNativeHeaders path and the C++ writeHead paths that mutate uWS's fromAncientRequest gate, which is also reached from Bun.serve via writeFetchHeadersToUWSResponse), so it's worth a human look.
Extended reasoning...
Overview
Fixes HTTP/1.0 response framing in node:http so the body encoding matches the advertised Transfer-Encoding header. Two changes:
src/js/node/_http_server.ts(renderNativeHeaders): when the user removedContent-LengthanduseChunkedEncodingByDefault === false(HTTP/1.0), close-delimit instead of forcing chunked.src/jsc/bindings/NodeHTTP.cpp: addstransferEncodingHasChunked()(mirroring the existingconnectionValueHasClosepattern) and, in both header-write paths, clearshttpResponseData->fromAncientRequestwhen aTransfer-Encodingheader containing thechunkedtoken is written, so the uWS body writer chunk-frames to match the head.- Five new HTTP/1.0 raw-socket tests asserting exact head/body bytes.
I traced fromAncientRequest through packages/bun-uws/src/HttpResponse.h — it gates only body-framing decisions (lines ~143/513/562/635/677); connection-close semantics are carried by the separate HTTP_CONNECTION_CLOSE state flag, which is set from httpRequest->isAncient() in HttpContext.h and is untouched here. So clearing the flag should not leak keep-alive behavior onto HTTP/1.0 connections.
Security risks
HTTP body-framing mismatches are the classic response-splitting/desync vector. This PR reduces that risk (before, the head said chunked while the body was identity), but it does so by mutating a shared uWS state flag that gates several code paths. The transferEncodingHasChunked token match is intentionally loose (mirrors Node's /(?:^|\W)chunked(?:$|\W)/i), which is correct for compat but worth a second pair of eyes.
Level of scrutiny
Medium-high. This is production HTTP wire-protocol code spanning a built-in JS module and C++ JSC bindings, and writeFetchHeadersToUWSResponse (one of the two patched sites) is also invoked from the Bun.serve path via WebCore__FetchHeaders__toUWSResponse, so the blast radius isn't strictly limited to node:http. Not a mechanical/config change I'd rubber-stamp.
Other factors
- Well-tested: five new tests, verified failing on release/ASAN without the fix and passing with it.
- No CODEOWNERS coverage for these files.
- No prior human review yet; CI build was just triggered.
- Companion to #33871 (HTTP/1.1 TE-value framing), so a maintainer may want to look at both together.
|
Re blast radius into |
|
The diff is green: Remaining CI red is unrelated Windows flake on lanes this change does not touch:
None of these exercise |
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-09, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. The linked issue (#19789) stays open. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
What does this PR do?
Fixes #19789
Fixes
node:httpserver responses to HTTP/1.0 clients where the head advertisedTransfer-Encoding: chunkedbut the body bytes were written as identity, desyncing any client that honours the header.Reproduction
Before this PR,
/rmclcarriedTransfer-Encoding: chunkedin the head (Bun invented it) with identity body bytes"hello", and/whtecarried the user'sTransfer-Encoding: chunkedheader with identity body bytes"hello". A real HTTP/1.0 client fails hard:Node.js close-delimits
/rmcl(noTransfer-Encoding) and chunk-frames/whteas5\r\nhello\r\n0\r\n\r\n.Cause
Two owners decide head and body independently and never look at each other:
renderNativeHeadersinsrc/js/node/_http_server.tsfalls through toforceChunkedwhen onlyContent-Lengthwas removed, without checkinguseChunkedEncodingByDefault(set tofalsefor HTTP/1.0 in theServerResponseconstructor). Node's_storeHeadertakes the!useChunkedEncodingByDefaultclose-delimited branch before it ever reaches the auto-chunked path.NodeHTTPServer__writeHeadforwards a user-setTransfer-Encoding: chunkedheader to the wire, but the uWS body writer gates every chunk-framing path on!fromAncientRequest, so the body is written as identity while the head says chunked.Fix
renderNativeHeaders: when the user removedContent-LengthanduseChunkedEncodingByDefaultisfalse, close-delimit the response instead of forcing chunked.NodeHTTP.cpp: when aTransfer-Encodingheader whose value contains thechunkedtoken is written, clearfromAncientRequestso the uWS writer chunk-frames the body to match the head (like Node). Other TE values (gzip,identity) leave the gate intact and the body stays raw.Verification
test/js/node/http/node-http-transfer-encoding.test.tsadds five HTTP/1.0 cases asserting the exact raw head and body bytes against Node:removeHeader('Content-Length'), explicitTransfer-Encoding: chunkedviasetHeader/writeHead/ streamingwrite, and aTransfer-Encoding: gzipregression guard. Four fail on the released binary and all pass after; the first two vendoredtest-http-1.0*.jstests andtest-http-remove-header-stays-removed.jsare unchanged.This is the HTTP/1.0 counterpart to #33871 (HTTP/1.1 TE value vs body framing); different code path (
fromAncientRequestgate vsContent-Length/TE-value conflict).[review] gate passed · iteration 1 · 3 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 1
evidence per changed file