Skip to content

node:http: queue pipelined responses on fallback connections instead of throwing ERR_HTTP_SOCKET_ASSIGNED - #36991

Open
robobun wants to merge 8 commits into
mainfrom
farm/0dee5a5e/http1-fallback-pipelining
Open

node:http: queue pipelined responses on fallback connections instead of throwing ERR_HTTP_SOCKET_ASSIGNED#36991
robobun wants to merge 8 commits into
mainfrom
farm/0dee5a5e/http1-fallback-pipelining

Conversation

@robobun

@robobun robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Problem

On the JS HTTP/1 fallback path (server.emit('connection', duplex) on a plain http.Server, and http2's allowHTTP1 ALPN fallback), a pipelined or fast keep-alive second request crashes with an uncaught ERR_HTTP_SOCKET_ASSIGNED (on the allowHTTP1 TLS path it surfaces as clientError and the connection is killed with no response bytes):

const server = http.createServer((req, res) => setImmediate(() => res.end("ok")));
const [clientSide, serverSide] = duplexPair();
server.emit("connection", serverSide);
clientSide.write("GET /a HTTP/1.1\r\nHost: x\r\n\r\nGET /b HTTP/1.1\r\nHost: x\r\n\r\n");
error: Socket already assigned
      at assignSocket (node:_http_server:2501:29)
      at onHttp1HeadersComplete (internal:http1_server_fallback:280:21)
      at onHttp1SocketData (internal:http1_server_fallback:334:31)

Node v26 serves both requests in order.

Cause

connectionListenerHTTP1 (internal/http1_server_fallback, added in #34432) calls res.assignSocket(socket) unconditionally for every parsed request. The previous response only releases the socket from its 'finish' listener, and 'finish' is deferred a tick, so whenever a second request's headers complete before that (always, for two requests in one TCP segment), assignSocket throws. The throw escapes parser.execute() inside the socket's 'data' handler, so it surfaces as an uncaughtException. A synchronous res.end() in the handler does not avoid it.

The native (uWS) server already handles this with a per-connection response queue (kPipelinedQueuedState / advanceResponsePipeline in _http_server.ts, mirroring Node's parserOnIncoming outgoing queue and resOnFinish); the fallback path never used it.

Fix

Reuse that queue for fallback connections, matching Node's _http_server.js shape:

  • _http_server.ts: extract the dispatcher's inline queue setup into queuePipelinedResponse() and the native socket close path's abort loop into abortQueuedPipelinedResponses(), and teach advanceResponsePipeline() to advance a non-native socket: no native startPipelinedResponse to call (each fallback response handle writes to the socket itself), assign via the prototype assignSocket so the 'close' listener a plain stream needs is installed, and clear finished before assigning so _flush does not emit an early 'prefinish' for a response that was ended while queued. The native branch is unchanged.
  • internal/http1_server_fallback.ts: on headers-complete, queue the response when socket._httpMessage is still set instead of calling assignSocket; after the finish path detaches the socket, either end the connection when the response advertised Connection: close (kMustCloseConnection, Node's resOnFinish _last branch; the queued responses are then aborted by the close path, RFC 9112 9.6) or advance the pipeline so the next queued response is assigned and its buffered writes are replayed; on socket close, abort the in-flight request and the responses (and their requests) still queued, like the native path and Node's socketOnClose.
  • Port Node's parserOnIncoming read gate to the fallback: pause reads when the transport or the bytes buffered on queued responses pass the socket's high water mark, resume from the pipeline advance or socket 'drain' (Node's socketOnDrain). Without it a pipelining client could buffer responses without bound; with it dispatch stops at the high water mark like Node.

The helpers are shared with the fallback module through internal/http (registered when _http_server initializes), so node:_http_server's exports are unchanged.

Buffering while queued (write/end/1xx, backpressure accounting, res.socket === null) comes from the existing kPipelinedQueuedState machinery, so both server paths now share one pipelining implementation. One pinned divergence from Node v26: when a response is destroyed while queued, Node assigns the destroyed message and wedges the connection until requestTimeout; Bun resets the connection (as the native path already did), since an HTTP/1.1 connection cannot skip a response slot.

Verification

New tests fail on main and pass with this change:

  • test/js/node/http/node-http.test.ts: two pipelined GETs with an async handler; out-of-order completion where the queued response is written (write + end, chunked) before the first finishes; three pipelined requests answered synchronously; in-flight and queued requests/responses aborted ('close') when the connection dies; a Connection: close response ends the connection and the queued response behind it is aborted, never sent; connection reset when a queued response is destroyed, with requests behind it aborted; read gate pauses dispatch once queued response bytes pass the high water mark and everything drains after release.
  • test/js/node/http2/node-http2.test.js (allowHTTP1 ALPN fallback over real TLS): pipelined requests served in order; queued response and request aborted when the connection dies.

Wire output for the ordering scenarios is byte-identical to Node v26.3.0, including header order and chunked framing.

No regressions in: node-http.test.ts, node-http2.test.js, node-http-connect.test.ts, backpressure/pause/transfer-encoding/abort suites, and the vendored test-http-generic-streams, test-http-pipeline-flood, test-http-pipeline-socket-parser-typeerror, test-http-get-pipeline-problem, test-http-incoming-pipelined-socket-destroy, test-http-keep-alive-pipeline-max-requests, test-http-many-ended-pipelines, test-http-pipeline-assertionerror-finish, test-http-pipeline-outgoing-destroy, test-http-pipeline-requests-connection-leak, test-http2-https-fallback, test-http2-https-fallback-http-server-options, test-http-server-unconsume-consume.


[review] gate passed · iteration 4 · 6 files touched

fails on main (without fix)
ASAN without fix: 7 failed, 7 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/http/node-http.test.ts "test/js/node/http2/node-http2.test.js"
bun test v1.4.0 (d02dd2f9b)

test/js/node/http/node-http.test.ts:
(pass) node:http > createServer > hello world [420.84ms]
(pass) node:http > createServer > is not marked encrypted (#5867) [69.93ms]
(pass) node:http > createServer > request & response body streaming (large) [118.78ms]
(pass) node:http > createServer > request & response body streaming (small) [71.85ms]
(pass) node:http > createServer > listen should return server [22.27ms]
(pass) node:http > createServer > listen callback should be bound to server [28.89ms]
(pass) node:http > createServer > should use the provided port [36.93ms]
(pass) node:http > createServer > should assign a random port when undefined [30.02ms]
(pass) node:http > createServer > option method should be uppercase (#7250) [47.82ms]
(pass) node:http > response > set-cookie works with getHeader [4.04ms]
(pass) node:http > response > set-cookie works with getHeaders [6.59ms]
(pass) node:http > request > should not insert extraneou
... (truncated)

release without fix: 1 failed, 7 skipped
bun test v1.4.0-canary.1 (44cef049d)

test/js/node/http/node-http.test.ts:
(pass) node:http > createServer > hello world [11.37ms]
(pass) node:http > createServer > is not marked encrypted (#5867) [3.53ms]
(pass) node:http > createServer > request & response body streaming (large) [4.38ms]
(pass) node:http > createServer > request & response body streaming (small) [2.21ms]
(pass) node:http > createServer > listen should return server [0.78ms]
(pass) node:http > createServer > listen callback should be bound to server [1.73ms]
(pass) node:http > createServer > should use the provided port [1.60ms]
(pass) node:http > createServer > should assign a random port when undefined [1.30ms]
(pass) node:http > createServer > option method should be uppercase (#7250) [2.32ms]
(pass) node:http > response > set-cookie works with getHeader [0.07ms]
(pass) node:http > response > set-cookie works with getHeaders [0.09ms]
(pass) node:http > request > should not insert extraneous accept-encoding header [2.30ms]
(pass) node:http > request > multiple Set-Cookie headers works #6810 [10.89ms]
(pass) node:http > request > should make a standard GET request when passed string as first arg [
... (truncated)
passes on PR (with fix)
ASAN with fix: 7 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/http/node-http.test.ts "test/js/node/http2/node-http2.test.js"
bun test v1.4.0 (d02dd2f9b)

test/js/node/http/node-http.test.ts:
(pass) node:http > createServer > hello world [417.01ms]
(pass) node:http > createServer > is not marked encrypted (#5867) [62.41ms]
(pass) node:http > createServer > request & response body streaming (large) [108.17ms]
(pass) node:http > createServer > request & response body streaming (small) [65.38ms]
(pass) node:http > createServer > listen should return server [24.17ms]
(pass) node:http > createServer > listen callback should be bound to server [23.12ms]
(pass) node:http > createServer > should use the provided port [33.74ms]
(pass) node:http > createServer > should assign a random port when undefined [25.71ms]
(pass) node:http > createServer > option method should be uppercase (#7250) [42.45ms]
(pass) node:http > response > set-cookie works with getHeader [3.31ms]
(pass) node:http > response > set-cookie works with getHeaders [5.74ms]
(pass) node:http > request > should not insert extraneou
... (truncated)

release with fix: 7 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 681ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/126] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 239 extern-C blocks audited
[2/126] gen cpp.rs (cppbind)
[3/126] gen JS modules (bundle-modules)
Preprocess modules (8917ms)
Bundle modules (47ms)
Postprocesss modules (259ms)
Bundle Functions (712ms)
Generate Code (24ms)

[9.98s] Bundled "src/js" for production
  2575 kb
  193 internal modules
  13 native modules
  84 internal functions across 17 files
[3/125] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
�[1m�[92m   Compiling�[0m bun_ast_jsc v0.0.0 (/workspace/bun/src/ast_jsc)
�[1m�[92m   Compiling�[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
�[1m�[92m   Compiling�[0m bun_semver_jsc v0.0.0 (/workspace/bun/src/semver_jsc)
�[1m�[92m   Compiling�[0m bun_sys_jsc v0.0.0 (/workspace/bun/src
... (truncated)
diff hotspot
src/js/internal/http.ts                  |  14 ++
 src/js/internal/http1_server_fallback.ts |  70 ++++++++-
 src/js/node/_http_server.ts              | 164 +++++++++++++++------
 test/js/node/http/node-http-proxy.js     |   7 +-
 test/js/node/http/node-http.test.ts      | 237 +++++++++++++++++++++++++++++++
 test/js/node/http2/node-http2.test.js    |  64 +++++++++
 6 files changed, 500 insertions(+), 56 deletions(-)

gate history · 3 passed · 1 rejected · iteration 4

evidence per changed file
file                                      reads  edits  tests
src/js/internal/http.ts                       4      7      0
src/js/internal/http1_server_fallback.ts      8     14      0
src/js/node/_http_server.ts                   8     15      0
test/js/node/http/node-http-proxy.js          1      1      0
test/js/node/http/node-http.test.ts           4      6      0
test/js/node/http2/node-http2.test.js         2      3      0

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

HTTP/1 response pipeline handling now shares queueing, advancement, abort, and backpressure helpers across native and fallback sockets. The fallback server queues responses during socket reuse and advances them after completion. Tests cover ordering, backpressure, connection cleanup, upgrades, and proxy address binding.

Changes

HTTP/1 response pipeline

Layer / File(s) Summary
Shared response pipeline wiring
src/js/internal/http.ts, src/js/node/_http_server.ts
Adds the http1ServerPipeline registry and registers reusable queueing, advancement, abort, and fallback read-flow helpers.
Native and fallback pipeline execution
src/js/internal/http1_server_fallback.ts, src/js/node/_http_server.ts
Queues and advances responses for both socket types. Fallback reads pause under backpressure and resume after drain. Queued responses and requests are aborted on connection close.
Pipelined response regression coverage
test/js/node/http/node-http.test.ts, test/js/node/http2/node-http2.test.js, test/js/node/http/node-http-proxy.js
Tests response ordering, chunked and synchronous responses, backpressure, connection reset behavior, close events, and IPv4 proxy binding.

Possibly related PRs

Suggested reviewers: cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: queueing pipelined responses on HTTP fallback connections.
Description check ✅ Passed The description explains the problem, cause, fix, scope, verification steps, test coverage, and known unrelated CI failures.

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

Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/node/_http_server.ts Outdated
…ion dies

Share the pipelining helpers with internal/http1_server_fallback through
internal/http instead of the node:_http_server module exports, and mirror
the native socket close path for responses still queued on a foreign
duplex: destroy them and their requests so both emit 'close'.
Comment thread src/js/internal/http.ts Outdated
Comment thread src/js/internal/http1_server_fallback.ts Outdated
Comment thread src/js/internal/http1_server_fallback.ts Outdated
Comment thread src/js/internal/http1_server_fallback.ts Outdated
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — ff2ea1b addresses the two concerns from my earlier pass: abortQueuedPipelinedResponses now runs from the fallback socket 'close' listener (with a hang-guard test), and the pipeline helpers moved to internal/http so node:_http_server's exports are unchanged. I didn't find anything new this round, but this reworks advanceResponsePipeline's dispatch for both socket kinds and there's an acknowledged follow-up on the flood guard plus the comment-cop nits, so worth a human look before landing.

What was reviewed:

  • queuePipelinedResponse / abortQueuedPipelinedResponses extractions are byte-identical moves; native #onClose and dispatcher paths behave the same.
  • advanceResponsePipeline's new instanceof NodeHTTPServerSocket branch keeps the native code path unchanged; releasePipelineOutgoingData on a foreign duplex is a no-op via optional chaining, not a throw.
  • Fallback 'finish'detachSocketadvanceResponsePipeline chain and the finished=false-before-assignSocket ordering for already-ended queued responses.
Extended reasoning...

Overview

This PR fixes an uncaught ERR_HTTP_SOCKET_ASSIGNED on the JS HTTP/1 fallback server path (server.emit('connection', duplex) and http2's allowHTTP1 ALPN fallback) when pipelined requests arrive. It does so by having the fallback path share the native path's per-connection response queue: three helpers (queuePipelinedResponse, advanceResponsePipeline, abortQueuedPipelinedResponses) are extracted in _http_server.ts and registered onto an internal object in internal/http; the fallback's headers-complete handler queues instead of assigning when socket._httpMessage is set, its 'finish' handler advances the pipeline, and its 'close' handler aborts the queue. advanceResponsePipeline gains a non-native-socket branch that skips startPipelinedResponse and clears finished before assignSocket so _flush doesn't emit an early 'prefinish'. Four files touched, two new tests.

Since my last review

ff2ea1b addressed both actionable items from my earlier pass: the queued-response abort loop is extracted and wired into the fallback 'close' listener (covered by the new "aborts queued pipelined responses when the connection dies" test, which hangs without the fix), and the helpers are shared through http1ServerPipeline in internal/http rather than the node:_http_server default export. The flood-guard gap I flagged is acknowledged as a deliberate follow-up, with a reasonable rationale (needs pausePipelineReads/releasePipelineOutgoingData to learn a foreign-duplex branch, and pre-existing backpressure gaps in the fallback handle).

Security risks

Low-to-moderate. The fallback path is network-reachable via http2 allowHTTP1, and the deferred flood guard means a pipelining client can queue unbounded responses on that path — but that's strictly no worse than main (which crashes on the second request), and the author has scoped it as a follow-up. No auth/crypto/permissions surface.

Level of scrutiny

Medium-high. This is core Node-compat HTTP server dispatch — advanceResponsePipeline runs for every pipelined response on every native connection too, and the change restructures its control flow around an instanceof check. I verified the native branch's body is unchanged relative to the pre-PR code, and the two extractions are pure moves. The fallback branch's finished=false clearing before assignSocket is subtle enough (it interacts with _flush/prefinish and the second finished=false clear before op replay) that a maintainer familiar with the ServerResponse finish machinery should confirm the ordering.

Other factors

  • The comment-cop bot flagged eight multi-line comments across the diff. Most explain Node-compat mirroring rather than justify workarounds, but they run 4-7 lines each against the repo's one-line preference; the author may want to trim them.
  • Test coverage is good: three pipelining scenarios (delayed, out-of-order chunked, synchronous chain) plus the abort-on-close case, all over duplexPair() so they exercise the fallback path specifically.
  • The http1ServerPipeline late-registration pattern (populated when _http_server loads, read after the fallback's require('node:http')) works because module loading is synchronous, but is a slightly unusual sharing mechanism — worth a maintainer's nod.

Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/internal/http1_server_fallback.ts Outdated
Comment thread src/js/node/_http_server.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks — ff2ea1b addresses both actionable findings from my earlier pass (queued responses now aborted on fallback socket close via the shared abortQueuedPipelinedResponses, and the pipeline helpers moved off the node:_http_server export surface into internal/http), and the flood-guard deferral is reasonable given the pre-existing backpressure gap on this path. No new issues found this round.

That said, this is a non-trivial refactor of advanceResponsePipeline — it now branches on socket type and manipulates res.finished ahead of assignSocket to keep _flush from firing an early 'prefinish' on the fallback path — plus a module-load-order dependency for the http1ServerPipeline late registration. Worth a human look given it's core HTTP/1 pipelining and reachable via the http2 allowHTTP1 ALPN fallback.

What was reviewed: the extracted queuePipelinedResponse/abortQueuedPipelinedResponses are byte-identical moves from their original inline sites; the ops-replay loop after the new fallback assignSocket still runs and re-sets finished via the replayed end(); the fallback close listener now mirrors the native #onClose abort loop; the new abort test would hang without the fix.

Extended reasoning...

Overview

This PR fixes an uncaught ERR_HTTP_SOCKET_ASSIGNED crash on the JS HTTP/1 fallback path (server.emit('connection', duplex) and http2's allowHTTP1 ALPN fallback) when pipelined requests arrive. It does so by having the fallback path reuse the native dispatcher's per-connection pipelined-response queue (kPipelinedQueuedState / kPipelinedResponses). Concretely: _http_server.ts extracts the inline queue setup into queuePipelinedResponse(), extracts the native socket's close-time abort loop into abortQueuedPipelinedResponses(), and teaches advanceResponsePipeline() a non-native-socket branch. http1_server_fallback.ts calls these instead of unconditionally assignSocket. The helpers are shared via a late-populated http1ServerPipeline object in internal/http rather than as node:_http_server exports.

Since my previous review, ff2ea1b addressed the 🔴 (queued responses leaked on fallback socket close — now aborted via the shared helper, with a new hang-if-broken test) and the 🟡 export-surface nit (helpers moved to internal/http). The remaining 🟡 (flood-guard not carried over) was deferred with a reasoned explanation that a correct port also needs pausePipelineReads/releasePipelineOutgoingData to learn a non-native pause/resume branch, and that the fallback handle already ignores transport backpressure for the in-flight response — both pre-existing and better done together. 9bbddc8 tightened comments.

Security risks

Low but non-zero. The http2 allowHTTP1 ALPN fallback is network-reachable, and this PR enables unbounded per-connection response queueing on that path without the kOutgoingData read-pause flood guard the native path applies. However, before this PR the same input crashed the process outright, so this is strictly an improvement; the flood-guard gap is acknowledged as a follow-up. No new parsing of untrusted input; no auth/crypto/permissions changes.

Level of scrutiny

Moderate-to-high. This is core node:http server pipelining — response ordering, socket-lifecycle transitions, and the finished/'prefinish' state machine are all subtle and historically bug-prone. The advanceResponsePipeline change introduces an instanceof NodeHTTPServerSocket branch where the fallback arm clears res.finished before assignSocket (so _flush doesn't emit an early 'prefinish'), then relies on the shared ops-replay loop to call res.end() and re-set it. I traced this and it looks correct (the replay loop's own finished = false reset is redundant but harmless on this path), but the interaction with user ServerResponse subclasses overriding assignSocket, and with the _flush'prefinish''finish' chain, deserves human eyes.

The http1ServerPipeline late-registration pattern depends on require('node:http') having already loaded _http_server before the fallback destructures the helpers — the fallback's own require('node:http') guarantees this, and it's commented, but it's an unusual coupling.

Other factors

Test coverage is good: four scenarios including async-handler pipelining, out-of-order completion with buffered chunked writes, three synchronous requests, and the abort-on-close case I flagged. The extracted queuePipelinedResponse and abortQueuedPipelinedResponses are verbatim moves from their inline originals, so the native path's behavior is unchanged. The comment-cop bot left several generic "paragraph-long comment" flags; the flagged comments explain genuinely non-obvious behavior (Node's parserOnIncoming queue semantics, why finished must be cleared before assignSocket) and read as appropriate to me, but that's a style call for a human.

Not approving because this touches critical HTTP server internals with subtle state-machine interactions — well beyond the "simple, mechanical, or obvious" bar.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

CI state across the seven builds on this PR (89328 through 89382, latest at d02dd2f: 195/196 jobs green): every lane that runs this diff is green, including all new pipelining tests on every platform. The one recurring red is unrelated to this change: test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts fails on the debian 13 x64-asan lane with an ASAN leak report in boringssl RSA keygen that also reproduces on main (tracked separately). Ready for review.

… queued-destroy and allowHTTP1 pipelining behavior

Mirror Node's parserOnIncoming read gate on fallback duplexes: pause reads
when the transport or the bytes buffered on queued pipelined responses pass
the high water mark, resume from the pipeline advance or socket drain. Gate
req._read so body reads cannot defeat the pause.

Also pin two behaviors with tests: a destroyed queued response resets the
connection (deliberate divergence from Node v26's wedge, matching the native
path), and the http2 allowHTTP1 ALPN fallback serves pipelined requests in
order and aborts queued ones when the connection dies.
Comment thread src/js/internal/http.ts
Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@test/js/node/http/node-http.test.ts`:
- Around line 4325-4327: Replace the unbounded polling loop after releaseFirst()
with a completion promise that resolves when the Nth HTTP/1.1 200 response is
observed. Wire clientSide error events and premature close events to reject the
same promise, then await it after releaseFirst(), preserving the test’s existing
response-count condition.

In `@test/js/node/http2/node-http2.test.js`:
- Around line 3930-3959: The test promise created in the HTTP/2 fallback test
currently ignores TLS socket failures. Capture its reject handler from
Promise.withResolvers and wire socket.on("error") to reject the test promise,
while retaining the existing aborted-resolution behavior and cleanup.
🪄 Autofix

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: ASSERTIVE

Plan: Pro

Run ID: 7611fe61-5d27-4dd7-9305-462492125db2

📥 Commits

Reviewing files that changed from the base of the PR and between 711cce0 and 5bcf6ae.

📒 Files selected for processing (5)
  • src/js/internal/http.ts
  • src/js/internal/http1_server_fallback.ts
  • src/js/node/_http_server.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/http2/node-http2.test.js

Comment thread test/js/node/http/node-http.test.ts Outdated
Comment thread test/js/node/http2/node-http2.test.js
robobun added 2 commits August 5, 2026 23:16
…on address family

A bare localhost can bind ::1 while the client resolves 127.0.0.1 first;
neither Node nor Bun falls back across families, so the test failed with
ECONNREFUSED on dual-stack hosts where the two resolutions diverge.
Comment thread src/js/internal/http1_server_fallback.ts
Mirror the native socket close path and Node's socketOnClose abortIncoming:
destroy socket._httpMessage's request (ConnResetException when it has an
error listener) before aborting the queued pipelined responses, so req
'close' fires for the in-flight request too.
Comment thread src/js/internal/http1_server_fallback.ts
Comment thread src/js/internal/http1_server_fallback.ts
…se instead of advancing the pipeline

Node's resOnFinish _last branch and the native onResponseFinishHandleSocket
end the connection when the finished response advertised Connection: close
(kMustCloseConnection); the fallback finish path only advanced, so a queued
pipelined response was replayed after the final response (RFC 9112 9.6).
Gate the advance on the flag and destroySoon/end the socket; the close path
then aborts the queued responses like the native path.
Comment thread src/js/internal/http1_server_fallback.ts
@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:52 PM PT - Aug 5th, 2026

@robobun, your commit d02dd2f has 1 failures in Build #89382 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36991

That installs a local version of the PR into your bun-36991 executable, so you can run:

bun-36991 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No further issues on d02dd2f — the kMustCloseConnection gate in onFallbackResponseFinish and its test look right. Deferring to a human: this reworks the shared pipelining path (advanceResponsePipeline now branches on socket type) and adds queue/abort/read-gate semantics to the network-reachable allowHTTP1 fallback.

Checked this run: the Connection: close gate ends the socket before advancing (queued responses are aborted via the close listener, matching Node's resOnFinish _last branch); the native-path refactor (queuePipelinedResponse / abortQueuedPipelinedResponses extraction) is behavior-preserving vs the inline code it replaced; releasePipelineOutgoingData's new fallback socket.resume() branch is guarded so native sockets still route through response.resume(); the res.finished = false reset before fallback assignSocket is scoped to queued.ended so it can't affect an un-ended queued response.

Extended reasoning...

Overview

The PR fixes an uncaught ERR_HTTP_SOCKET_ASSIGNED crash when pipelined requests arrive on the JS HTTP/1 fallback path (server.emit('connection', duplex) and http2 allowHTTP1). It extracts the native dispatcher's per-connection response queue (queuePipelinedResponse, abortQueuedPipelinedResponses) into shared helpers registered on an internal http1ServerPipeline object, teaches advanceResponsePipeline to handle non-native sockets, and wires the fallback connection listener to queue/advance/abort responses and gate reads on outgoing backpressure. ~230 lines of source across _http_server.ts, http1_server_fallback.ts, and internal/http.ts, plus ~260 lines of new tests covering ordering, out-of-order completion, connection-death abort, Connection: close termination, destroyed-queued-slot reset, and the read gate.

Security risks

The allowHTTP1 fallback is network-reachable over TLS, so the read-gate addition (maybePauseFallbackReads) is a DoS mitigation — without it a pipelining client could buffer queued-response bytes without bound. The gate mirrors Node's parserOnIncoming check and is covered by a test that asserts the pause and subsequent drain. No new parsing of untrusted input; the change reuses the existing HTTPParser and renderNativeHeaders paths.

Level of scrutiny

High. This is production node:http server code on a hot path, and advanceResponsePipeline is now shared between native and fallback connections with a new instanceof NodeHTTPServerSocket branch — a mistake here affects every native pipelined response too. Three prior automated review rounds each found a real issue (missing queued-response abort on close, missing read-gate, missing in-flight request abort, missing kMustCloseConnection gate before advance), all now fixed with tests.

Other factors

Test coverage is thorough (six new tests plus two http2 fallback tests, wire output verified byte-identical to Node v26). The native-path changes are refactor-only (inline code → named functions with identical bodies). The one deliberate Node divergence (reset the connection when a queued response is destroyed, vs Node wedging until requestTimeout) is documented and tested. All prior review comments (mine, CodeRabbit's, comment-cop's) are resolved. The remaining unresolved comment-cop note on line 354 is the automated "paragraph-long comment" heuristic firing on an explanatory comment, not a code issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants