Bun.serve: answer 206 for a Bun.file() slice as long as the file that starts past byte 0
Stacked on #44826. Draft until the two questions in Downsides have an answer.
Problem
Bun.file(p).slice(5, 21)of a 16-byte file sends 11 bytes and answers200with noContent-Range.slice(5, 20)andslice(5, 22)send the same 11 bytes and answer206withContent-Range: bytes 5-15/*.do_sendfile(src/runtime/server/RequestContext.rs:1906) sets theneeds_content_rangebit from the slice length as asked, before the clamp. A slice as long as the file counts as the whole file.- Over TLS, v1.3.12 answered
206withContent-Rangehere. #29202 (v1.3.13) changed that.
Fix
do_sendfilesets the bit after the clamp, from the window that it sends.render_metadatareads the bit alone. The blob-size store and the second check against it have no reader left and are deleted.- Verified: the tests of #44826 now pin the status of each row, plus 3 rows in
serve.test.ts. On #44826 they fail on status lines only. - Self-reviewed together with #44826: 2 concerns on this change, both addressed (Notes).
Background
- The bit asks for
206and an automaticContent-Range. Only a path-backed file gets it. render_metadatahas three arms. No init headers: any status becomes206. Headers withoutContent-Range: the status stays. Headers withContent-Range: any status becomes206.- The hunk is the one in draft #43910.
Downsides
- Question 1: should a slice as long as the file from past byte 0 answer
206, as over TLS through v1.3.12? - Question 2 (#1668): in the first and third arm an explicit status is replaced, as main does for every other slice.
{ status: 404 }and{ status: 416, "Content-Range": "bytes */16" }on these slices become206. Should an explicit status win?
Notes
History (handler of docs/runtime/http/routing.mdx, "To send part of a file", Range: bytes=100-307300 on 307200 bytes, official release binaries and debug builds of 620b50f6ab).
| build | plain HTTP | TLS |
|---|---|---|
| v1.3.0, v1.3.12 | 200, length 307200, 307100 bytes | 206, length 307100, bytes 100-307199/* |
| v1.3.13, main | 200, length 307200, 307100 bytes | 200, length 307200, 307100 bytes |
| #44826 | 200, length 307100 | 200, length 307100 |
| this PR | 206, length 307100, bytes 100-307199/* | 206, length 307100, bytes 100-307199/* |
Status by arm, path slice(5, 15) of 10 bytes (as long as the file, from byte 5).
Response init | main | #44826 | this PR | slice(5, 14) on main |
|---|---|---|---|---|
| none | 200 | 200 | 206, bytes 5-9/* | 206, bytes 5-9/* |
{ status: 404 } (also 201, 500, 416) | 404 | 404 | 206, bytes 5-9/* | 206, bytes 5-9/* |
{ headers: { "x-a": "1" } } | 200 | 200 | 200 | 200 |
{ status: 404, headers: { "x-a": "1" } } | 404 | 404 | 404 | 404 |
{ headers: { "Content-Range": "bytes 5-9/10" } } | 200 | 200 | 206 | 206 |
{ status: 416, headers: { "Content-Range": "bytes */10" } } | 416 | 416 | 206, bytes */10 | 206, bytes */10 |
Each cell of this PR equals what the neighbour slice(5, 14) answers on main with the same init. On main every row of the first three columns is also mis-framed (length 10 for 5 bytes). #44826 fixes the length.
Counts.
| probe | main | #44826 | this PR |
|---|---|---|---|
| 24 explicit statuses (404, 500, 201, 416) over 6 slices, no headers: replaced by 206 | 20 | 20 | 24 |
| the same 24 with one unrelated header: replaced | 0 | 0 | 0 |
| 36-row matrix (path and fd, GET and HEAD): rows whose status changes | - | 0 | 3 |
| 23 slice shapes: GET status differs from main | - | 0 | 7 |
| rows of both probes that are not a slice as long as the file and that change | - | 0 | 0 |
What also follows.
- An empty window at the end of the file (
slice(10, 20)of 10 bytes) answers206, length 0,Content-Range: bytes 10-10/*, asslice(10, 19)does on main. The test does not pin that text. - GET answers
206where HEAD answers the status of the handler, as for every other path slice on main (#41585). Bun.file(fd)slices keep the status of the handler. An incomingRangeheader on an unslicedBun.file(fd)still answers206, as on main.- A read of
response.headers, or a print of the Response, attaches a header list and so moves the Response from the first arm to the second.new Response(Bun.file(p).slice(0, 100))answers206, and200with noContent-Rangeafterres.headers.get("content-type")orBun.inspect(res). This is the same on main and with this PR. It is a separate defect and not changed here. - If the answer to question 1 is yes, the example in
docs/runtime/http/routing.mdxis right again for such aRange. It can also return the file unsliced:do_sendfileanswersRangefor a whole file.
Tests. bun-serve-file.test.ts: the status of each row of the 18-window test, the shrink case, the TLS window, the documented handler and the sendfile row. serve.test.ts, should support Content-Range with Bun.file(): rows [10, size + 10] (two tests) and [size, 2 * size]. All 42 cases of that block pass. Not run: Windows, macOS, HTTP/2, HTTP/3.
Self-review. A comment on the auto_close gate said that a descriptor of the caller gets no automatic Content-Range. An incoming Range on an unsliced Bun.file(fd) gets one, so the comment is gone. The rows with an explicit status were not pinned by any test. They are now, one per arm.
Code. +5/-14 in RequestContext.rs on top of #44826. Release builds: do_sendfile 4571 to 4546 bytes (8 bodies), render_metadata 2321 and 2334 to 2251 and 2294 (2 bodies).
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/serve.test.ts, test/js/bun/http/bun-serve-file.test.ts