#43106·bun

Bun.serve: a Bun.file() route sends a Transfer-Encoding or Content-Length header from its Response verbatim

Author: robobunCreated Sep 17, 2026Updated Sep 17, 2026

What version of Bun is running?

1.4.3-canary.1+c6b7fcb5b. The code on main at 630e921db0 is the same.

What platform is your computer?

Linux x64

What steps can reproduce the bug?

// file-framing.mjs
import net from "node:net";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const dir = fs.mkdtempSync(path.join(os.tmpdir(), "file-framing-"));
const p = path.join(dir, "a.txt");
fs.writeFileSync(p, "file-body");

const server = Bun.serve({
  port: 0,
  hostname: "127.0.0.1",
  routes: {
    "/file-te": new Response(Bun.file(p), { headers: { "Transfer-Encoding": "chunked" } }),
    "/file-cl": new Response(Bun.file(p), { headers: { "Content-Length": "3" } }),
    "/static-te": new Response("file-body", { headers: { "Transfer-Encoding": "chunked" } }),
    "/static-cl": new Response("file-body", { headers: { "Content-Length": "3" } }),
  },
  fetch: () => new Response("fallback"),
});

async function raw(pathname) {
  const { promise, resolve } = Promise.withResolvers();
  let buf = "";
  const s = net.connect(server.port, "127.0.0.1", () =>
    s.write(`GET ${pathname} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n`),
  );
  s.on("data", d => (buf += d.toString("latin1")));
  s.on("error", () => {});
  s.on("close", resolve);
  await promise;
  const [head, ...body] = buf.split("\r\n\r\n");
  const framing = head.split("\r\n").filter(l => /^(content-length|transfer-encoding):/i.test(l));
  console.log(pathname.padEnd(12), JSON.stringify({ framing, body: body.join("\r\n\r\n") }));
}
for (const r of ["/file-te", "/file-cl", "/static-te", "/static-cl"]) await raw(r);
server.stop(true);
fs.rmSync(dir, { recursive: true, force: true });

Run bun file-framing.mjs.

What is the expected behavior?

The route computes the framing from the file. All four routes send Content-Length: 9 and no Transfer-Encoding. The two static Response routes already do that.

What do you see instead?

/file-te     {"framing":["Transfer-Encoding: chunked","content-length: 9"],"body":"file-body"}
/file-cl     {"framing":["Content-Length: 3"],"body":"file-body"}
/static-te   {"framing":["Content-Length: 9"],"body":"file-body"}
/static-cl   {"framing":["Content-Length: 9"],"body":"file-body"}
  • /file-te sends Transfer-Encoding: chunked together with content-length, and the body is not chunk-encoded. RFC 9112 6.2 forbids the two headers in one message. A client that obeys Transfer-Encoding reads file-body as a chunk size and fails.
  • /file-cl declares 3 bytes and sends 9. On a keep-alive connection the client reads the other 6 bytes as the start of the next response.

Additional information

StaticRoute::from_js (src/runtime/server/StaticRoute.rs:209) removes Transfer-Encoding and Content-Length from the Response headers before it takes the snapshot. FileRoute::from_js (src/runtime/server/FileRoute.rs:132) takes the snapshot unchanged, and FileRoute::write_headers writes every entry. has_content_length_header then stops the route from writing the length it computed from the file.

A Response that copies the headers of a fetched response is a likely source of such a header. #40902 (open) adds the same strip for { dir, headers } routes.

Found during the review of the static route Connection: close fix. That change does not touch this behavior.