Skip to content

Bun.serve: deliver the in-flight response when a pipelined request arrives while it is still pending - #35036

Closed
robobun wants to merge 10 commits into
mainfrom
farm/e4197cfa/serve-pipelined-async-response
Closed

robobun wants to merge 10 commits into
mainfrom
farm/e4197cfa/serve-pipelined-async-response

Conversation

@robobun

@robobun robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

On Windows, two pipelined HTTP/1.1 requests to a Bun.file() route in one TCP segment close the connection with zero bytes written. On any platform, the same happens with an async fetch handler:

import { connect } from "node:net";
const s = Bun.serve({ port: 0, async fetch() {
  await new Promise(r => setImmediate(r));
  return new Response("hello");
}});
const sock = connect(s.port, "127.0.0.1", () => {
  sock.write("GET / HTTP/1.1\r\nHost: x\r\n\r\nGET / HTTP/1.1\r\nHost: x\r\n\r\n");
});
let buf = ""; sock.on("data", d => buf += d);
sock.on("close", () => console.log("len", buf.length));
// before: len 0
// after:  len 120 (first response, then close)

The Windows Bun.file() route case (routes: { "/": new Response(Bun.file(p)) }) is the same bug: on POSIX FileResponseStream reads a regular file synchronously inside the request handler, so the response completes before the parser moves on; on Windows the read goes to the libuv threadpool and completes on a later loop tick, so the second pipelined request is parsed while HTTP_RESPONSE_PENDING is still set.

Cause

uWS has one HttpResponseData per socket. When a pipelined request is parsed while the previous response is still pending, the !IsNodeHttp branch in HttpContext<SSL>::onData's per-request callback did:

us_socket_close((us_socket_t *) s, 0, nullptr);
return nullptr;

The socket is still corked at that point, so the first response's bytes never reach the wire.

Fix

HttpContext.h: mark the in-flight response for connection-close, re-arm its idleTimeout, and pause reads instead of closing. The pipelined request is dropped without dispatch; when the first handler eventually ends the response, the close-after-drain path shuts the socket down. RFC 9112 9.3.2: a pipelining client must be prepared to retry unanswered requests on a new connection. The check is hoisted above the per-request us_socket_timeout(s, 0) so a dropped request cannot disarm the idle timeout, and pause() stops further segments so a pipelined flood cannot keep extending it. Synchronous pipelining (handler responds before returning) is unchanged.

HttpResponse.h: two follow-throughs so the close mark is acted on and cannot corrupt an upgrade.

  • cork() early-returned without running its close-after-drain check when the handler had already uncorked via internalEnd(); an async handler completing outside onData with the close mark would leave the socket open until idle timeout. The check now runs on that early-return when the socket is still in the HTTP group (excludes the WebSocket-upgrade case, per-socket).
  • internalEnd()'s uncorked close check is gated on !keepCorked. Only upgrade() passes keepCorked=true; closing there would destruct HttpResponseData mid-upgrade() and double-free HttpParser::fallback (confirmed under ASAN). upgrade() also resume()s the socket so the adopted WebSocket can read after the drop path paused it.

The IsNodeHttp (node:http) branch already queues pipelined responses and is unchanged.

#32868 takes the same approach against the pre-IsNodeHttp HttpContext.h and conflicts with current main. #33664 goes further and buffers pipelined bytes to serve every request in order (full async pipelining for Bun.serve); it also conflicts with current main. This PR applies the minimal RFC-9112-conforming degradation to the current structure.

Verification

New tests:

  • test/js/bun/http/serve.test.ts: pipelined GETs behind an async fetch handler (first response delivered, second handler not called, socket closes), and pipelined POST-with-body plus a third request (all dropped past the first). idleTimeout: 0 plus a bounded close-wait so a regression in the close-after-drain path is a named failure, not an idle-timeout pass.
  • test/js/bun/http/bun-serve-file.test.ts: pipelined requests to a Bun.file() route. Before (Windows): firstLine === "". After: at least the first response reaches the wire; POSIX still serves both (sync file read).
  • test/js/bun/websocket/websocket-server.test.ts: WS handshake + pipelined GET + partial third request, async server.upgrade(). Before (with only the HttpContext.h change): ASAN double-free of HttpParser::fallback. After: upgrade succeeds and the server keeps answering.

Also ran: serve.test.ts (full), bun-serve-file.test.ts (full, Linux + Windows), request-smuggling.test.ts, http-server-chunking.test.ts, bun-serve-headers.test.ts (Connection: close cases), bun-serve-static.test.ts, hspec.test.ts, websocket-server.test.ts, node-http.test.ts, test-http-keep-alive*.js, test-http-pipeline*.js, test-http-upgrade-server.js. No new failures.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/http/bun-serve-file.test.ts test/js/bun/http/serve.test.ts test/js/bun/websocket/websocket-server.test.ts

…rives while it is still pending

uWS has one HttpResponseData per socket. When a pipelined request is
parsed while the previous response is still pending (async fetch handler,
or a Bun.file() body whose read goes through the libuv threadpool on
Windows), the connection was hard-closed with the cork buffer discarded
and zero bytes reached the client.

Drop the pipelined request and mark the in-flight response for
connection-close instead. RFC 9112 9.3.2: a pipelining client retries
unanswered requests on a new connection.

cork()'s early-return when the handler already uncorked (internalEnd)
skipped the close-after-drain check; gate it on !isParsingHttp so an
async handler completing outside onData still closes once marked.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 157d829b-8ba1-4173-a2e1-ba06cd03c4cb

📥 Commits

Reviewing files that changed from the base of the PR and between d656c9b and c8c3cf2.

📒 Files selected for processing (4)
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpResponseData.h
  • src/uws_sys/Response.rs
  • test/js/bun/http/serve.test.ts

Walkthrough

Changes

The HTTP server now preserves in-flight responses when dropping pipelined requests. Upgrade and cork handling retain socket ownership safely, with regression tests covering file responses, async handlers, and WebSocket handshakes.

HTTP pipelining and WebSocket upgrade

Layer / File(s) Summary
Preserve in-flight pipelined responses
packages/bun-uws/src/HttpContext.h, packages/bun-uws/src/HttpResponseData.h, src/uws_sys/Response.rs, test/js/bun/http/serve.test.ts, test/js/bun/http/bun-serve-file.test.ts
Pending responses are preserved while subsequent pipelined requests are dropped, with connection-state updates and coverage for async handlers and file-backed routes.
Preserve upgrade socket ownership
packages/bun-uws/src/HttpResponse.h, test/js/bun/websocket/websocket-server.test.ts
Upgrade completion keeps the socket corked during ownership transfer, resumes reads, guards cleanup against socket changes, and tests pipelined WebSocket handshakes.

Possibly related PRs

  • oven-sh/bun#32488: Updates overlapping HTTP pipelining and socket close/uncork behavior.

Suggested reviewers: cirospaciari, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix for pipelined requests while a response is still pending.
Description check ✅ Passed The description covers the bug, cause, fix, repro, and verification, though it uses custom headings instead of the template sections.
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.

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

@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:56 AM PT - Jul 22nd, 2026

✅ @robobun, your commit 5609c00046d5d3fad722cc3c16107a5ae28f7861 passed in Build #77599! 🎉


🧪   To try this PR locally:

bunx bun-pr 35036

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

bun-35036 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 4 issues this PR may fix:

  1. Bun.file as response behaving weirdly #6961 - Bun.serve + Response(Bun.file()) causes subsequent keep-alive requests to hang because async file read leaves the response pending, and the next request triggers the hard-close path
  2. Bun.file routes 404 #22174 - Bun.serve Bun.file routes go pseudo-dead (404) under stress due to pipelined requests hitting the pending-response hard-close path
  3. Response(Bun.file) streams look like HTTP/0.9 over LAN  #26406 - Bun.serve + Response(Bun.file()) produces HTTP/0.9-like broken responses over LAN because higher latency widens the premature-close race window during async file reads
  4. Response(Bun.file()) is slower than Response(await Bun.file().text()) #11228 - Response(Bun.file()) is slower than Response(await file.text()) because pipelined keep-alive requests get hard-closed during async file reads, forcing new TCP connections

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #6961
Fixes #22174
Fixes #26406
Fixes #11228

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. http: deliver the in-flight response when a pipelined request arrives behind an async handler #32868 - Fixes the same pipelined-request zero-byte bug in HttpContext.h; PR Bun.serve: deliver the in-flight response when a pipelined request arrives while it is still pending #35036 explicitly supersedes this one
  2. Bun.serve: defer pipelined HTTP/1.1 requests behind an async response instead of closing the socket #33664 - Addresses the identical Bun.serve pipelined HTTP/1.1 async response zero-byte abort, same code area with a different implementation strategy

🤖 Generated with Claude Code

Comment thread packages/bun-uws/src/HttpResponse.h Outdated
Comment thread packages/bun-uws/src/HttpContext.h Outdated
Comment thread test/js/bun/http/bun-serve-file.test.ts Outdated
…sParsingHttp

The isParsingHttp flag lives on HttpContextData (one per server), so it
answered 'is some socket inside onData' rather than 'is this socket'. A
socket whose async response completes during another socket's onData
(via drain_microtasks) would have its close check skipped and stay open
until idle timeout.

The socket onData corked reaches here via the already-corked else branch,
so the guard only needs to exclude the upgrade case (c): compare the
socket's group before/after handler() (in-place adopt changes the group;
a reallocated adopt marks the old allocation closed).

Also fold the file-route test's stderr/exitCode into a combined object
assertion per review.

@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.

The per-server isParsingHttp guard from the previous revision is now a per-socket group comparison in cork() — verified it captures httpGroup before handler() and skips the close check when the socket was adopted into the WebSocket group or closed, so the cross-socket delayed-close corner from the earlier review is resolved. The standalone expect(stderr).toBe("") was also folded into the combined-object assertion.

Extended reasoning...

Commit 2fabfd0 addressed both points from the previous review round: the cork() close-after-drain check now gates on us_socket_is_closed + us_socket_group == httpGroup (per-socket) instead of !isParsingHttp (per-server), and the bun-serve-file test now asserts { firstLine, body, stderr, exitCode } as one object. I re-read the new guard against the three enumerated INVALID_CORK_SLOT cases (large-write uncork, stolen empty slot, upgrade) and it correctly excludes (b) via HTTP_RESPONSE_PENDING and (c) via the group change / closed check. The remaining concern is the new inline finding on the async-WebSocket-upgrade path in internalEnd(), which is separate from the cork() guard.

Comment thread packages/bun-uws/src/HttpContext.h Outdated
Comment thread test/js/bun/http/bun-serve-file.test.ts
… harden tests

internalEnd(): gate the uncorked close check on !keepCorked. Only upgrade()
passes keepCorked=true, and closing there would destruct HttpResponseData
while upgrade() still reads and destructs it (confirmed double-free of
HttpParser::fallback under ASAN when a partial third request is pipelined
behind the handshake). upgrade() also resume()s the socket so the adopted
WebSocket can read after the drop path paused it.

Drop path: move the check above the per-request timeout clear so a dropped
pipelined request cannot disarm the in-flight request's idleTimeout, and
resetTimeout()+pause() so a pipelined flood cannot keep extending it across
segments. The IsNodeHttp pending branch is left where it was; the hoisted
!IsNodeHttp check makes its Bun.serve arm unreachable.

Tests: add an ASAN regression test for the async-upgrade double-free; extend
the serve.test.ts pipelined coverage to a POST body + third request; bound
the close wait and set idleTimeout: 0 so a regression in the close-after-
drain path is a named failure, not an idle-timeout pass; wire socket 'error'
in the file-route fixture.
Comment thread packages/bun-uws/src/HttpContext.h Outdated
…s armed

HttpResponse::pause() does Super::timeout(0) after Super::pause(), so the
previous order overwrote the re-arm.

@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: 1

🤖 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 `@packages/bun-uws/src/HttpContext.h`:
- Around line 395-403: Replace the __builtin_unreachable() in the !IsNodeHttp
branch of the HTTP_RESPONSE_PENDING/hasQueuedPipelinedResponses handling with an
assertion that explicitly validates the invariant. Preserve the surrounding
node:http queued-pipelining behavior while ensuring any future invariant
violation fails through a defined assertion rather than undefined behavior.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: fb25c15f-a4df-44b4-b7d0-fd85cf3dedb1

📥 Commits

Reviewing files that changed from the base of the PR and between f0d2eaf and d656c9b.

📒 Files selected for processing (5)
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpResponse.h
  • test/js/bun/http/bun-serve-file.test.ts
  • test/js/bun/http/serve.test.ts
  • test/js/bun/websocket/websocket-server.test.ts

Comment thread packages/bun-uws/src/HttpContext.h Outdated
For !IsNodeHttp, HTTP_RESPONSE_PENDING is handled above and
hasQueuedPipelinedResponses stays false, so gating the whole block on
IsNodeHttp makes the else branch the only path without a runtime-unreachable
arm.
Comment thread packages/bun-uws/src/HttpContext.h Outdated
Comment thread packages/bun-uws/src/HttpContext.h Outdated
…elined drop

The HTTP_CONNECTION_CLOSE bit no longer only means 'the client already knows'
now that the pipelined-drop path sets it on a keep-alive client. Drop the
(state & HTTP_CONNECTION_CLOSE) == 0 term from internalEnd()'s header guard
(END_CALLED is the write-once guard) so the final response tells the client
it is final (RFC 9112 9.6). Assert the header in the pipelined-GETs test.
Comment thread packages/bun-uws/src/HttpContext.h
Comment thread packages/bun-uws/src/HttpResponse.h Outdated
robobun and others added 2 commits July 22, 2026 05:55
…rs behind a dropped request

Dropping the (state & HTTP_CONNECTION_CLOSE) == 0 header guard regressed
node:http (writeAutoHeaders() writes Connection: close via raw write, so
internalEnd() wrote a second one) and missed the copy-pasted guard in
uws_res_end_without_body(). Use a distinct HTTP_PIPELINED_DROP bit that
shouldCloseConnection() reads; the header-write guards keep checking
HTTP_CONNECTION_CLOSE ('client already knows / already written') so the
header is emitted after a drop and not duplicated on node:http.

Returning s from the drop path lets the parser keep validating the dropped
request's bytes. A parse error there wrote a 4xx via us_socket_write() and
closed, which the client reads as the first (valid) request's answer and
aborts the in-flight response. Skip the 4xx write/close when
HTTP_PIPELINED_DROP is set; the connection is already paused and marked for
close-after-drain.

@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 findings after the HTTP_PIPELINED_DROP rework in 0484107, but this reworks the core Bun.serve request-dispatch and close-after-drain path in vendored uWS (cork early-return, internalEnd() keepCorked gate, upgrade() resume, parse-error suppression), so it's worth a human look.

What was reviewed:

  • HTTP_PIPELINED_DROP as a separate bit: internalEnd()'s HTTP_CONNECTION_CLOSE guard is restored, so node:http's writeAutoHeaders() no longer double-writes Connection: close; uws_res_end_without_body() now sees should_close_connection()==true via the Rust-side is_http_connection_close() change.
  • Parse-error path after a drop: unrefs, uncorks, and returns without the 4xx write/close — the in-flight response's cork buffer survives; the !IsNodeHttp gate keeps node:http's clientError path unchanged.
  • cork() early-return close check: gated on !is_closed && group unchanged, so a reallocated/in-place-adopted WebSocket ext block is not read as HttpResponseData.
  • resetResponseState() clears HTTP_PIPELINED_DROP (not in HTTP_CONNECTION_SCOPED), so a sync-pipelined request after a completed response starts clean.
Extended reasoning...

Overview

The PR changes how Bun.serve handles a pipelined HTTP/1.1 request that arrives while the previous async response is still pending. Previously the socket was hard-closed with the cork buffer discarded (zero bytes written); now the pipelined request is dropped, the connection is marked for close-after-drain via a new HTTP_PIPELINED_DROP state bit, reads are paused, and the in-flight response completes before the socket closes. Follow-through changes: cork() runs the close-after-drain check on its early-return path (gated on the socket still being in the HTTP group), internalEnd()'s uncorked close check is gated on !keepCorked so upgrade() doesn't destruct the ext block mid-adopt, upgrade() resumes reads, and the parse-error path skips the 4xx write when a drop already happened. The Rust State bitflags mirror gained the new bit and is_http_connection_close() reads it. Three new tests cover async-handler pipelining, Bun.file route pipelining (the Windows repro), and async server.upgrade() behind a pipelined handshake (the ASAN double-free the keepCorked gate prevents).

Security risks

No new attack surface. The change is strictly more defensive than the previous behavior (which hard-closed). The pause() + resetTimeout() on drop bounds a pipelined-flood client the same way a lone async request already is; the parse-error suppression only fires when HTTP_PIPELINED_DROP is already set, and that path still marks the connection for close. isConnectRequest = false on drop prevents a dropped CONNECT from switching the in-flight response into tunnel handling.

Level of scrutiny

High — this is the vendored uWebSockets HTTP dispatch/response path that every Bun.serve request goes through, and the changes interact with corking, socket adoption (WebSocket upgrade), the node:http compat instantiation, and parse-error handling. The PR went through five rounds of review feedback, each of which surfaced a real cross-path interaction (dead resetTimeout(), Connection: close header suppression, node:http duplicate-header regression, 4xx-overwrite on trailing parse error). All are addressed and the current diff looks correct to me, but the density of subtle interactions here warrants a maintainer's eyes.

Other factors

All prior inline comments are resolved with fix commits. Test coverage is good: the three new tests exercise the drop path, the Connection: close header on the delivered response, the parse-error-after-drop case, and the upgrade-after-drop ASAN case; the PR description lists the broader suites that were re-run. HTTP_PIPELINED_DROP is not in HTTP_CONNECTION_SCOPED, so resetResponseState() clears it — a sync-pipelined next request on the same socket won't inherit it (though in practice the drop path always leads to close). The IsNodeHttp path is unchanged.

@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 issues found in the current revision — all earlier review threads (dead resetTimeout(), vacuous if constexpr, Connection: close header emission, node:http duplicate-header fallout, and the trailing-parse-error 4xx overwrite) are addressed via the separate HTTP_PIPELINED_DROP bit and the gated error path. That said, this reworks Bun.serve's per-socket connection lifecycle across onData, cork(), internalEnd(), and upgrade(), with memory-safety-adjacent interactions (ext-block destruction on adopt, close-after-drain vs. WebSocket group move), so it warrants a human look before landing.

What was reviewed:

  • Verified HTTP_PIPELINED_DROP is read by both C++ shouldCloseConnection() and Rust is_http_connection_close(), and that internalEnd()'s header-write guard is restored so node:http's writeAutoHeaders() de-dupe still holds.
  • Checked the new cork() early-return close path is gated on !is_closed && group unchanged so an in-place or reallocated WebSocket adopt doesn't reinterpret the ext block.
  • Checked resetResponseState() clears HTTP_PIPELINED_DROP (not in HTTP_CONNECTION_SCOPED) — fine since the drop path never dispatches a next request on this socket.
Extended reasoning...

Overview

The PR changes how Bun.serve handles a pipelined HTTP/1.1 request that arrives while the previous response is still pending. Previously the socket was hard-closed (dropping the in-flight response's corked bytes); now the pipelined request is dropped, the connection is marked for close-after-drain via a new HTTP_PIPELINED_DROP state bit, reads are paused, and the in-flight response completes normally before the socket closes. Touches HttpContext.h (per-request callback + parse-error path), HttpResponse.h (internalEnd !keepCorked gate, cork() early-return close check, upgrade() resume), HttpResponseData.h (new bit + shouldCloseConnection()), and src/uws_sys/Response.rs (mirror bit). Three new tests cover async-fetch pipelining, Bun.file route pipelining (Windows repro), and async server.upgrade() behind a pipelined request (ASAN double-free guard).

Security risks

The change sits on a DoS-protection path (async pipelining rejection). The new behavior pauses reads and re-arms idleTimeout, so a pipelined flood cannot hold the socket open indefinitely — same exposure as a lone async request. The parse-error path now suppresses the 4xx write when HTTP_PIPELINED_DROP is set; that's scoped to !IsNodeHttp and only after a request was already dropped, so it doesn't open a smuggling vector (the connection is already marked for close and reads are paused). No auth/crypto surface.

Level of scrutiny

High. This is core Bun.serve connection-lifecycle C++ with cross-cutting effects on cork()/uncork(), WebSocket upgrade() (which destructs the ext block in place), the node:http instantiation, and the Rust render path via should_close_connection(). The PR went through five rounds of review, each surfacing a non-obvious interaction (dead resetTimeout store, node:http Connection: close duplication, 4xx overwriting the in-flight response), which is exactly the profile where a maintainer familiar with uWS's cork-slot and adopt semantics should sign off.

Other factors

Test coverage for the new paths is thorough (three targeted tests plus a Connection: close header assertion and a malformed-trailing-bytes case). Two alternative PRs (#32868, #33664) are called out in the description with different design trade-offs (buffer-and-serve vs. drop-and-close); a maintainer should confirm this minimal-degradation approach is the one to land. uws_res_end_without_body in libuwsockets.cpp keeps its HTTP_CONNECTION_CLOSE guard unchanged, so bodyless responses after a drop now correctly emit Connection: close via is_http_connection_close() reading the new bit.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

CI on builds 77518 and 77599: every test failure is tagged [flaky] (passed on retry) and none touch the HTTP server/pipelining paths this PR changes (install, webview, repl, cron, gc-http-client, http2-client, cpu-prof). The remaining red is darwin-14-aarch64-test-bun which expired without running on both builds.

My new tests (serve.test.ts pipelined describe block, bun-serve-file.test.ts pipelined route, websocket-server.test.ts async upgrade behind pipelined) are green on every lane that ran. Ready for review.

robobun added a commit that referenced this pull request Aug 13, 2026
…d it

HttpContext::onData pauses the socket while request bytes are parked
behind a pending response. If that response turns out to be a WebSocket
upgrade, upgrade() destructs the HttpResponseData (and the parked bytes
with it, as for bytes trailing a synchronous upgrade) but us_socket_adopt
carries the paused flag over, so the WebSocket sent its 101 and then never
read a frame. Drop the parked bytes and resume before internalEnd(), so
markDone() does not arm a replay dispatch for them either.

Tests folded in from the earlier pipelining PRs (#33664, #35036, #32868)
for the cases bun-serve-pipelining.test.ts did not cover yet: a Bun.file()
body from the handler and a Bun.file() route over each transport, a held
request that carries a body with a Connection: close request behind it,
held bytes that are a parse error, and the upgrade case above.
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #38128, which is current with main (this branch conflicts) and goes further for the same close in HttpContext.h: instead of delivering the in-flight response and then closing, it holds the pipelined request and answers it once the response ahead of it completes.

This PR's tests were run against #38128: the in-flight response is delivered in every case as they assert; the assertions that differ are the ones pinning the close and the undispatched second request, which #38128 answers instead. Two findings from here were carried over into test/js/bun/http/bun-serve-pipelining.test.ts on #38128: the Bun.file() route case (as a 16 MiB file so it exercises the asynchronous path on every platform, not only Windows) and the async server.upgrade() with a request pipelined behind it, which also turned up a missing resume() in upgrade() on that branch that is now fixed there. Any further work on this bug should go to #38128.

@robobun robobun closed this Aug 13, 2026
robobun added a commit that referenced this pull request Aug 23, 2026
…d it

HttpContext::onData pauses the socket while request bytes are parked
behind a pending response. If that response turns out to be a WebSocket
upgrade, upgrade() destructs the HttpResponseData (and the parked bytes
with it, as for bytes trailing a synchronous upgrade) but us_socket_adopt
carries the paused flag over, so the WebSocket sent its 101 and then never
read a frame. Drop the parked bytes and resume before internalEnd(), so
markDone() does not arm a replay dispatch for them either.

Tests folded in from the earlier pipelining PRs (#33664, #35036, #32868)
for the cases bun-serve-pipelining.test.ts did not cover yet: a Bun.file()
body from the handler and a Bun.file() route over each transport, a held
request that carries a body with a Connection: close request behind it,
held bytes that are a parse error, and the upgrade case above.
robobun added a commit that referenced this pull request Aug 25, 2026
…d it

HttpContext::onData pauses the socket while request bytes are parked
behind a pending response. If that response turns out to be a WebSocket
upgrade, upgrade() destructs the HttpResponseData (and the parked bytes
with it, as for bytes trailing a synchronous upgrade) but us_socket_adopt
carries the paused flag over, so the WebSocket sent its 101 and then never
read a frame. Drop the parked bytes and resume before internalEnd(), so
markDone() does not arm a replay dispatch for them either.

Tests folded in from the earlier pipelining PRs (#33664, #35036, #32868)
for the cases bun-serve-pipelining.test.ts did not cover yet: a Bun.file()
body from the handler and a Bun.file() route over each transport, a held
request that carries a body with a Connection: close request behind it,
held bytes that are a parse error, and the upgrade case above.
robobun added a commit that referenced this pull request Aug 27, 2026
…d it

HttpContext::onData pauses the socket while request bytes are parked
behind a pending response. If that response turns out to be a WebSocket
upgrade, upgrade() destructs the HttpResponseData (and the parked bytes
with it, as for bytes trailing a synchronous upgrade) but us_socket_adopt
carries the paused flag over, so the WebSocket sent its 101 and then never
read a frame. Drop the parked bytes and resume before internalEnd(), so
markDone() does not arm a replay dispatch for them either.

Tests folded in from the earlier pipelining PRs (#33664, #35036, #32868)
for the cases bun-serve-pipelining.test.ts did not cover yet: a Bun.file()
body from the handler and a Bun.file() route over each transport, a held
request that carries a body with a Connection: close request behind it,
held bytes that are a parse error, and the upgrade case above.
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