Bun.serve: stream the body for a 307/308 Bun.file() route instead of hanging keep-alive - #35024
Bun.serve: stream the body for a 307/308 Bun.file() route instead of hanging keep-alive#35024robobun wants to merge 3 commits into
Conversation
…alive A Bun.file() route configured with a status the route drops the body for (205, 307, 308) wrote only status + headers via end_without_body: no Content-Length, no Connection: close. HTTP/1.1 keep-alive clients block waiting for body framing (RFC 9112 6.3 only self-terminates 1xx/204/304). Write Content-Length: 0 for the three non-self-terminating statuses; 1xx/204/304 stay header-only.
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 9:54 PM PT - Jul 21st, 2026
❌ @robobun, your commit 54cb9ba has 2 failures in
🧪 To try this PR locally: bunx bun-pr 35024That installs a local version of the PR into your bun-35024 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
Self-review pointed out that only FileRoute dropped the body for 307/308; StaticRoute and both fetch-handler paths (file and in-memory) ship it. RFC 9110 15.4 permits a redirect body, and adding Content-Length: 0 on top of a FileRoute-only drop would have cemented the divergence. Instead, remove the 307|308 clause so 307/308 fall through to the normal sendfile/write() path (real Content-Length, real body), matching the other three paths. 205 stays bodiless (RFC 9110 15.3.6 forbids content on a 205) and gets Content-Length: 0.
There was a problem hiding this comment.
The earlier inline comment about wiring close/error on the raw-socket helpers is addressed in f3644fb — both now resolve(buf) on close and reject on error. Beyond the description-staleness nit, I also confirmed is_null_body covers 205 (so that branch is reachable), the has_written_content_length_header() guard prevents a duplicate header when the user set one, and the updated fd_guard comment is accurate now that 307/308 reach FileResponseStream::start.
Extended reasoning...
Previous review feedback was addressed; the only new finding is a nit on the stale PR title/description. The runtime change itself (307/308 now stream the file body rather than ending bodiless) is a user-visible behavioral change to Bun.serve — consistent with StaticRoute and the fetch-handler path per the code comment, but worth a maintainer's glance, so not auto-approving.
Pipelining a Bun.file() route on Windows closes the connection with zero bytes, independent of status (reproduces on main with status 200). The framing and fetch assertions already cover the fix on Windows.
There was a problem hiding this comment.
LGTM — small, well-reasoned framing fix that aligns FileRoute's 307/308 handling with the three sibling paths that already stream the body.
What was reviewed:
- Traced 307/308 fall-through:
status_code != 200skips Range, hits the existing content-length + FileResponseStream path;fd_guardownership transfer is unchanged. - Checked
is_null_bodyset (101/103/204/205/304) against RFC 9112 §6.3 — 205 is indeed the only one not self-terminating; 204/304 correctly stay header-only. - Verified the
!has_written_content_length_header()guard prevents a double CL when the user sets their own (viawrite_headers→mark_wrote_content_length_header). - Test now wires
close→resolve anderror→reject per earlier feedback; Windows pipelining skip is documented as a pre-existing bug.
Extended reasoning...
Overview
Two files: src/runtime/server/FileRoute.rs (~10 net lines) removes || matches!(status_code, 307 | 308) from the null-body early-return so 307/308 fall through to the normal FileResponseStream path, and adds a Content-Length: 0 write for status 205 before end_without_body. test/js/bun/http/bun-serve-file.test.ts gains one raw-socket test covering framing for 204/205/304/307/308, a pipelined-307 keep-alive check, and a fetch(redirect:'manual') completion assertion.
Security risks
None identified. The change makes HTTP/1.1 message framing more correct (adds Content-Length where it was missing, causing keep-alive hangs). No auth, crypto, permissions, or untrusted-input parsing is touched. 307/308 now send file bytes on the wire, but that matches what routes: + in-memory body, fetch: + Bun.file(), and fetch: + in-memory already do — so no new exposure surface.
Level of scrutiny
Medium — Bun.serve response framing is production-critical, but the runtime change is tiny and the design decision ("should a 307/308 file route send its body?") is not novel: the PR aligns the one outlier with three existing sibling paths and RFC 9110 §15.4. The 205 Content-Length: 0 is a mechanical RFC-compliance fix guarded against double-write. I traced the fall-through path (Range skipped because status_code != 200, fd_guard cleared before stream ownership transfer) and it reuses the existing 200 path unchanged.
Other factors
Two prior review rounds already landed fixes: raw-socket helpers now wire close/error correctly, and the PR title/description were re-synced after the approach changed mid-review. The PR description includes an audit of every remaining end_without_body call site. The Windows pipelining skip is documented as reproducing on main with status 200, so it's pre-existing and orthogonal. CI build #77346 is running on the head commit.
|
Build #77346: |
Repro
Raw wire before:
No
Content-Length, noTransfer-Encoding, noConnection: close, socket left open. RFC 9112 6.3: a response with none of those is delimited by the server closing the connection, so a keep-alive client blocks. Same forstatus: 205andstatus: 308.Cause
FileRoute::onended a null-body or 307/308 status before starting the file stream:uws_res_end_without_bodywrites only the header terminator andmarkDone(); it never writesContent-Length. Of the statuses this arm handles, 1xx/204/304 self-terminate under RFC 9112 6.3, but 205/307/308 do not.The 307/308 body drop is also a FileRoute-only behavior. The identical
new Response(body, { status: 307 })through the other three paths ships the body:routes:+Bun.file()routes:+ in-memoryfetch:+Bun.file()fetch:+ in-memoryRFC 9110 15.4 permits a body on a redirect response and browsers render it. The
307 | 308clause arrived with the original FileRoute PR without a stated reason.Fix
Remove
|| matches!(status_code, 307 | 308)so 307/308 fall through to the normal sendfile/write()path and get the realContent-Lengthplus body, same as every other serve path.205 remains in the bodiless arm (it is a WHATWG null-body status and RFC 9110 15.3.6 forbids generating content on a 205) and now writes
Content-Length: 0since it is the one null-body status RFC 9112 6.3 does not self-terminate. 1xx/204/304 stay header-only.Verification
New raw-socket test in
bun-serve-file.test.tsasserts 307/308 carry the file'sContent-Lengthand body (and that theroutes:path matches thefetch:path), 205 carriesContent-Length: 0, 204/304 stay header-only, two pipelined 307s both resolve on a keep-alive connection, andfetch(redirect:'manual')completes. All 84 existing tests in the file still pass.Other
end_without_bodysites auditedFollow-up to #35008's audit of the same class. Of the remaining sites that PR listed as out of scope:
FileRoute.rs:510(205/307/308)FileResponseStream.rs:500end_without_bodytoend(b"")change, open)RequestContext.rs:1120,FileRoute.rs:555RequestContext.rs:3411(render_production_error404)status=500NodeHTTPResponse.rs:1589(res.destroy())NodeHTTPResponse.rs:1494(res.emit('close')withoutend())server_body.rs:420(respond_stopped_503)js_value_for_dispatch; callers are inside uWS handlers where post-uncork closesno 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