Repository navigation
Conversation
… the header list A fetch handler that returns `new Response(Bun.file(path).slice(a, b))` gets 206 and a Content-Range. `render_metadata` chose between that and the status of the Response by whether the Response had a header list. The first read of `headers` allocates the list, and so does printing the Response. So the same Response went out as 200 with no Content-Range after a read, and with any header that the handler set. With no list, a status of the handler such as 404 went out as 206. A slice is now promoted to 206 when the status is 200 or 206, or when the handler set its own Content-Range. Any other status stays as the handler gave it. The header list no longer counts.
|
Updated 9:30 AM PT - Oct 19th, 2026
✅ @robobun, your commit b649d8c53c36b0de05dc7baaf2353a3ffa2d32d0 passed in 🧪 To try this PR locally: bunx bun-pr 46059That installs a local version of the PR into your bun-46059 --bun |
|
Status: reproduced on 1.4.3 (367d939). A fetch handler that returns CI: Buildkite build #128887 passed on all platforms at b649d8c. This PR (#46059) stays a draft until a maintainer answers the two questions in its description. |
Draft until the two questions in Downsides have an answer.
Problem
new Response(Bun.file(path).slice(0, 100)). Bun.serve answers206withContent-Range: bytes 0-99/*. Afterres.headersorconsole.log(res), it answers200with noContent-Range.render_metadata(src/runtime/server/RequestContext.rs:3882) chooses by whether the Response has a header list. The first read ofheadersallocates one.Fix
Content-Range. Any other status stays.FileRoute.rs:386).test/js/bun/http/bun-serve-file.test.ts(all fail on 1.4.3). Alsoserve.test.tsand 5 more suites.Background
Downsides
Content-Rangegoes from 200 to 206.{ status: 200 }cannot opt out. Is that wanted?{ status: 404 }on a slice with no headers goes from 206 to 404. Should an explicit status win?.text+256 bytes (release build). The status test runs only behindneeds_content_range.Notes
Answers before and after.
slice(0, 100)of a 1000-byte file, GET, release builds of main 91e7e3b and of this branch. The released 1.4.3 answers as main does. "206" means withContent-Range: bytes 0-99/*.new Response(slice)res.headers,res.headers.get(),[...res.headers]orBun.inspect(res)res.clone()after a read,new Response(slice, response)after a read ofresponse.headers{ status: 206 }, after a readContent-Range{ headers: { "x-a": "1" } },res.headers.set(),append(),delete(){ status: 206, headers: { "x-a": "1" } }Content-Range{ status: 404 }(also 201, 500), no headers, no read{ status: 404 }, after a read or with a header{ headers: { "Content-Range": "bytes 0-99/1000" } }, status 200 or 404RangeMiddleware. The docs example (
new Response(Bun.file(p).slice(0, 100))) behind Hono 4.7.2:compress()(adds no header to an.mp4, it only reads them)cors(),etag(),secureHeaders(){ status: 404 }compress()orcors(),{ status: 404 }Wire diff. A raw-socket script ran 477 rows on both release builds: 36 response shapes, 15 looks and writes, GET, HEAD and
Rangerequests, 9 copies, 6 entry points.Content-Range).fetch, a route function,error()), from 200 to 206.Content-Range(96 from 200 to 206, 19 gainedContent-Range). Question 1.Response.json,Response.redirect, whole file, static routes). No row differs in anything but the status line andContent-Range.Cost. The status test is two integer compares inside
needs_content_range && (...).do_sendfilesets that flag only for a path slice or an incomingRange(RequestContext.rs:1968,:2019). The control flow of every other response is the same as on main. 0 rows of the wire diff that are not a slice changed. Release builds of main 91e7e3b and of this branch, linux-x64, one build directory:.text81,665,765 -> 81,666,021 bytes (llvm-size -A), and the 8 bodies ofrender_metadata2342 and 2347 -> 2390 and 2432 bytes (nm -S).What does not change.
Bun.file(fd)slices keep the status of the handler.Content-Rangestill becomes 206 ({ status: 404, headers: { "Content-Range": ... } }).206withContent-Range: bytes 5-5/*and length 0, as a bareslice(5, 5)does on main. With this PR a header or a read no longer turns that into 200.If the answer to question 1 is no. Then the header list has to tell a read from a write. A variant of this PR did that: a mark on
WebCore::FetchHeadersfor the list that a read ofheadersmakes, and the update counter of the list to tell that nothing wrote to it. It changed only rows with a read. It was +166/-63 in 8 source files,sizeof(FetchHeaders)stayed 96 bytes, andResponse.json()cost 2845.7 -> 2852.2 instructions per call (noise 5 to 12). It needs a status gate so that{ status: 404 }plus a read stays 404. It is not in this branch.Related. #44826 and #44827 change the lines above this hunk (the
Content-Lengthand where the 206 bit is computed). #45601 changes how the header list is stored and leaves this rule as it is.Suites run on the debug build.
bun-serve-file(176 pass), theContent-Rangeblock ofserve.test.ts(39 pass),bun-serve-headers,bun-serve-static,bun-server,serve-reused-response,serve-directory-routes.serve.test.tshas the same 2 failures with and without the change in the build container (a privileged port test that runs as root, and a sendfile abort test that times out).serve-file-slice-read-error.test.tsneeds more than the 5 s default there and passes with a longer timeout.no test proof · iteration 0 · 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