Skip to content

Bun.serve: answer 206 for a Bun.file() slice as long as the file that starts past byte 0 - #44827

Draft
robobun wants to merge 1 commit into
robobun/b3a069fc/serve-file-slice-content-lengthfrom
robobun/b3a069fc/serve-file-slice-status
Draft

robobun wants to merge 1 commit into
robobun/b3a069fc/serve-file-slice-content-lengthfrom
robobun/b3a069fc/serve-file-slice-status

Conversation

@robobun

@robobun robobun commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

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 answers 200 with no Content-Range. slice(5, 20) and slice(5, 22) send the same 11 bytes and answer 206 with Content-Range: bytes 5-15/*.
  • do_sendfile (src/runtime/server/RequestContext.rs:1906) sets the needs_content_range bit 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 206 with Content-Range here. serve: consolidate file streaming into FileResponseStream; Range support; fix aborted-stream hang #29202 (v1.3.13) changed that.

Fix

Background

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 (Forced 206 Partial Content in response #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 become 206. 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 620b50f).

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) answers 206, length 0, Content-Range: bytes 10-10/*, as slice(10, 19) does on main. The test does not pin that text.
  • GET answers 206 where HEAD answers the status of the handler, as for every other path slice on main (Bun.serve: resolve a file body for HEAD the same way as for GET #41585).
  • Bun.file(fd) slices keep the status of the handler. An incoming Range header on an unsliced Bun.file(fd) still answers 206, 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)) answers 206, and 200 with no Content-Range after res.headers.get("content-type") or Bun.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.mdx is right again for such a Range. It can also return the file unsliced: do_sendfile answers Range for 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

@robobun

robobun commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:20 AM PT - Oct 9th, 2026

✅ @robobun, your commit 4bdf0f8160ebe7a5686f72125b7b1ef5e3fa3642 passed in Build #124078! 🎉


🧪   To try this PR locally:

bunx bun-pr 44827

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

bun-44827 --bun

@robobun

robobun commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Status: stacked on #44826 and reproduced on that branch. Bun.file(p).slice(5, 21) of a 16-byte file answers 200 with no Content-Range, and slice(5, 20) answers 206 with Content-Range: bytes 5-15/* for the same 11 bytes. Over TLS, v1.3.12 answered 206 for both. This PR stays a draft until a maintainer answers the two questions in the description.

…e clamp

`do_sendfile` sets the `needs_content_range` bit from the slice length as
asked. A path slice as long as the file that starts past byte 0 counts as
the whole file. It answers 200 with no Content-Range, and its neighbours
one byte shorter or longer answer 206.

The bit is now set after the clamp, from the window that is sent.
`render_metadata` reads the bit alone. The copy of the file size in the
blob and the second check against it have no reader left, so they are
deleted.
@robobun

robobun commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Related: #46059 proposes one rule for the three arms that Background describes here. A slice becomes 206 when the status is 200 or 206, or when the handler set Content-Range. Any other status stays.

  • It answers question 2 with "an explicit status wins", for every path slice.
  • It also removes the dependence on the header list. Today a read of res.headers allocates that list and turns the 206 into 200 (the separate defect in Notes here).
  • It asks one more question: a slice with a handler header and no Content-Range goes from 200 to 206.

If the answer to question 2 is no, #46059 does not apply as it is.

This branch has not been deployed

No deployments
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