What version of Elysia is running?
1.4.28 (regression introduced in 1.4.23)
What platform is your computer?
Linux x86_64 (also reproducible on macOS)
What environment are you using
bun 1.3.13
Are you using dynamic mode?
No
What steps can reproduce the bug?
import { Elysia } from 'elysia'
await Bun.write('/tmp/sample.bin', 'abcdefghij') // 10 bytes
new Elysia()
.get('/file', () => new Response(Bun.file('/tmp/sample.bin')))
.listen(3000)
curl -sI -H 'Range: bytes=2-5' http://localhost:3000/file
What is the expected behavior?
HTTP/1.1 206 Partial Content
content-range: bytes 2-5/10
content-length: 4
accept-ranges: bytes
This is what Bun.serve returns for a bare new Response(Bun.file()) — Bun's HTTP layer auto-handles HTTP byte-range requests when the response body is a Bun.file. It is also what Elysia 1.4.22 returned.
What do you see instead?
HTTP/1.1 200 OK
content-length: 10
Full file, no range support, no Accept-Ranges. Bun's auto-Range never fires.
Real-world impact
Mobile Safari refuses to play <video> elements when the server doesn't honor byte-range requests (Apple requires this). Every static asset served by @elysiajs/static is new Response(Bun.file()) under the hood, so the regression silently broke video playback on iOS for every Elysia app on Bun using the static plugin. Desktop browsers tolerate it — they just download the whole file — which is why this went unnoticed.
Downstream issues: elysiajs/elysia-static#19 (open since 2024), elysiajs/elysia-static#79 (open draft).
Root cause
Commit 0d9b0401 ("fix: can't modify immutable headers", in 1.4.23) changed createResponseHandler in src/adapter/utils.ts to always rewrap:
const newResponse = new Response(response.body, {
headers: mergeHeaders(response.headers, set.headers),
status: mergeStatus(response.status, set.status)
})
The original motivation was correct — on Node and Cloudflare Workers, response.headers is immutable, so mutating it in place threw. But the fix applies on Bun too, and reading response.body as a ReadableStream severs the Bun.file association that Bun.serve needs to provide auto-Range. Result: a regression that affects every Bun user but not Node/CF users.
Related
#1768 (open) reports the same root cause — createResponseHandler always rewrapping — for a different downstream symptom (ReadableStream cancel callback never invoked). The reporter quotes the same line in createResponseHandler. The same structural fix would resolve both.
Suggested fix
Take the mutate-in-place path only when (a) we're on Bun and (b) we don't actually need to rewrap (status unchanged, no streaming required). Otherwise fall through to the existing rewrap path so Node/CF still works. PR opening shortly.
Have you try removing the node_modules and bun.lockb and try again yet?
Yes — reproducible on a fresh install of elysia@1.4.28.
What version of Elysia is running?
1.4.28 (regression introduced in 1.4.23)
What platform is your computer?
Linux x86_64 (also reproducible on macOS)
What environment are you using
bun 1.3.13
Are you using dynamic mode?
No
What steps can reproduce the bug?
curl -sI -H 'Range: bytes=2-5' http://localhost:3000/fileWhat is the expected behavior?
This is what Bun.serve returns for a bare
new Response(Bun.file())— Bun's HTTP layer auto-handles HTTP byte-range requests when the response body is aBun.file. It is also what Elysia 1.4.22 returned.What do you see instead?
Full file, no range support, no
Accept-Ranges. Bun's auto-Range never fires.Real-world impact
Mobile Safari refuses to play
<video>elements when the server doesn't honor byte-range requests (Apple requires this). Every static asset served by@elysiajs/staticisnew Response(Bun.file())under the hood, so the regression silently broke video playback on iOS for every Elysia app on Bun using the static plugin. Desktop browsers tolerate it — they just download the whole file — which is why this went unnoticed.Downstream issues:
elysiajs/elysia-static#19(open since 2024),elysiajs/elysia-static#79(open draft).Root cause
Commit
0d9b0401("fix: can't modify immutable headers", in 1.4.23) changedcreateResponseHandlerinsrc/adapter/utils.tsto always rewrap:The original motivation was correct — on Node and Cloudflare Workers,
response.headersis immutable, so mutating it in place threw. But the fix applies on Bun too, and readingresponse.bodyas aReadableStreamsevers theBun.fileassociation that Bun.serve needs to provide auto-Range. Result: a regression that affects every Bun user but not Node/CF users.Related
#1768 (open) reports the same root cause —
createResponseHandleralways rewrapping — for a different downstream symptom (ReadableStreamcancelcallback never invoked). The reporter quotes the same line increateResponseHandler. The same structural fix would resolve both.Suggested fix
Take the mutate-in-place path only when (a) we're on Bun and (b) we don't actually need to rewrap (status unchanged, no streaming required). Otherwise fall through to the existing rewrap path so Node/CF still works. PR opening shortly.
Have you try removing the
node_modulesandbun.lockband try again yet?Yes — reproducible on a fresh install of
elysia@1.4.28.