Skip to content

Response header bytes past ASCII reach the client unchanged under Bun - #697

Merged
dannycochran merged 2 commits into
mainfrom
fix/response-header-bytes-past-ascii-under-bun
Oct 12, 2026
Merged

dannycochran merged 2 commits into
mainfrom
fix/response-header-bytes-past-ascii-under-bun

Conversation

@ronleizrowice-ant

@ronleizrowice-ant ronleizrowice-ant commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Bun's http.Server writes a response's head as a string, and names no encoding, on a connection it is handed with emit('connection'). So the socket's default applies, UTF-8. The proxy reads an upstream's headers as latin1, one char per byte, so every header byte from 0x80 up reaches the client as two: e9 as c3 a9, and c3 a9 as c3 83 c2 a9. Location is followed to another URL, Content-Disposition names another file, Set-Cookie comes back changed.

Affected, under Bun: TLS-terminated tunnels (since #665) and everything through srt proxy (#666). Not affected: Node, plain HTTP through the full proxy, request headers, bodies. In no release.

What changes (+23/-2)

  • A socket about to be emitted gets latin1 as its default encoding. Bun only: Node's server names latin1 itself. A Bun that comes to name it ignores the default.
  • A refusal's reason is written as bytes, so it never reaches such a socket as a string.

The rule this makes: what the proxy itself writes to such a socket with no encoding named is bytes or ASCII. It is today. A header value with a char past 0xFF cannot be cut to a control byte: all three runtimes refuse it (ERR_INVALID_CHAR) before anything is written.

Why not otherwise

A Buffer as a header value is refused on all three runtimes. setHeader, writeHead and raw header arrays all double. Writing the head by hand means redoing the framing.

Testing

  • Nine tests: all 128 bytes and those headers, byte for byte, twice per connection, on four paths; a non-ASCII refusal reason stays UTF-8. Four fail without the change. Eight of nine mutations fail a test; the ninth (no runtime gate) changes nothing under Bun.
  • curl in the real sandbox against a raw TLS upstream, on v0.0.79, main and here, on Bun 1.4.2, Bun 1.4.3 and Node 22: here equals v0.0.79 on all three.
  • 73 odd responses through a terminated tunnel, main against here: only the lines with high bytes differ.

Also here

Three comment lines in windows-sandbox-utils.ts: npx eslint . fails on main, where a lint rule (#683) and a call made on purpose (#686) met. #688 and #689 carry the same lines, so any of the three can merge first.

Not covered, seen on the way

  • Bun 1.4.2 only: a request that arrives while another is in flight on a terminated tunnel ends the connection (ERR_HTTP_SOCKET_ASSIGNED). 1.4.3 is right.
  • An unsolicited 101 from the upstream leaves the client unanswered, as in v0.0.79.

Bun's http.Server writes a response's head as a string with no encoding named
on a connection it is handed with emit('connection'), so the socket's default
applies. The proxy reads an upstream's headers as latin1, so as UTF-8 every
header byte from 0x80 up reached the client as two, on TLS-terminated tunnels
and through srt proxy. A socket about to be emitted gets latin1 as its
default, and a refusal's reason is written as bytes.
The rule against direct realpath calls and the one call that is made on
purpose met on main.

@dannycochran dannycochran left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/stamp

@dannycochran
dannycochran merged commit 24eb39e into main Oct 12, 2026
16 checks passed
@dannycochran
dannycochran deleted the fix/response-header-bytes-past-ascii-under-bun branch October 12, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants