Skip to content

uws: route every HTTP/1 header-section termination through terminateHeaders() - #43866

Open
robobun wants to merge 1 commit into
mainfrom
robobun/2e7ec37d/uws-terminate-headers
Open

robobun wants to merge 1 commit into
mainfrom
robobun/2e7ec37d/uws-terminate-headers

Conversation

@robobun

@robobun robobun commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #43853

Behaviour change: none

Problem

Fix

  • Add HttpResponse::writeOwnedHeaders(chunked): Date (unless the caller wrote one) and Transfer-Encoding: chunked for a chunked body (unless the caller wrote one). terminateHeaders(chunked) adds the blank line. Every block in HttpResponse.h and uws_res_prepare_for_sendfile now call it. The Content-Length path of internalEnd calls writeOwnedHeaders and keeps the blank line in the Content-Length write.
  • uws_res_end_without_body stays as it is: it writes no Date today, so routing it through would change the wire. The next PR does that.
  • Correct because the wire output is the same: a raw socket probe of 26 response paths, each with a keep-alive, a Connection: close and an HTTP/1.0 request, is byte-identical to a build of main (Date masked).
  • Verified: test/js/bun/http/bun-serve-headers.test.ts, bun-serve-static, bun-serve-file, request-smuggling, serve-direct-readable-stream, serve.test.ts, bun-server.test.ts, test/js/node/http/, websocket-server.test.ts.

Background

  • writeMark() writes the Date header once per response and records that in HTTP_WROTE_DATE_HEADER. The blocks this PR folds all called it right before the blank line.
  • Release .text of the stack: 80,656,785 to 80,655,761 bytes (size, -1,024). The HttpResponse<true|false> and uws_res_* symbols involved: 12,809 to 12,223 bytes (nm -S).
Notes
  • An uncorked internalEnd (a large in-memory body from an async handler, a Bun.file end from a read callback) issues the same send() calls as before: writeOwnedHeaders is writeMark(), and the Content-Length line plus the blank line stay one write.
  • uws_res_prepare_for_sendfile used getSendBuffer(2) for the blank line. Super::write("\r\n", 2) does the same when corked. When not corked, both end in one send for the two bytes: getSendBuffer took a cork slot and uncork() flushed it.
  • Static instruction counts on bun-profile (objdump): HttpResponse<false>::internalEnd 493 to 465, write 300 to 276, terminateHeaders 60 (new, shared).
  • Probe paths: in-memory, HEAD, async handler, 204, 304, ReadableStream, Bun.file small and above 1 MiB (sendfile), explicit Transfer-Encoding, user Connection: close, user Date, thrown error page, 404, static route GET/HEAD/POST, static route with Connection: close, file route GET/HEAD, file route above 1 MiB, node:http server default, Connection: close, removeHeader("connection"), chunked, HEAD, 204. Each with a keep-alive, a Connection: close and an HTTP/1.0 request.
  • Not routed through terminateHeaders(): writeContinue() (100 Continue) and the raw 1xx writer. They are informational responses with no header section of their own.
  • Open PR Bun.serve: send Connection: keep-alive and Keep-Alive: timeout=N by default #43850 edits the same blocks. After this PR its keep-alive header belongs in terminateHeaders().
  • websocket-server.test.ts has 4 tests that time out in this container on main too (send() > Buffer (utf-8) and siblings). serve.test.ts has 2 that need a non-root user or a non-loopback interface.

no test proof · iteration 1 · the description declares no behaviour change, so there is no failing test to prove; the existing suite in CI is the check

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 1cb8fb5e-8815-4a69-aa3d-5ae49753938e

📥 Commits

Reviewing files that changed from the base of the PR and between 81f8803 and c5b9d81.

📒 Files selected for processing (1)
  • packages/bun-uws/src/HttpResponse.h

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

HttpResponse adds shared methods to write owned headers and terminate response headers. Response-ending, streaming, upgrade, and header-flush paths use these methods. Sendfile preparation also calls terminateHeaders for SSL and non-SSL responses.

Changes

HTTP response header termination

Layer / File(s) Summary
Shared header termination
packages/bun-uws/src/HttpResponse.h, src/uws_sys/libuwsockets.cpp
writeOwnedHeaders writes the Date header and, for chunked responses, adds Transfer-Encoding: chunked if the caller has not supplied it. terminateHeaders writes the owned headers and ends the header section. Response paths use these helpers; the Content-Length path writes its header line and the blank line separately. Sendfile preparation calls terminateHeaders in both SSL and non-SSL branches, which continue to uncork the response.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to c5b9d

Header termination preserves the inspected HTTP response output; no issue identified here prevents merging after normal checks.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: routing HTTP/1 header-section termination through terminateHeaders().
Description check ✅ Passed The description explains the problem, implementation, unchanged behavior, verification steps, and relevant exceptions. It provides the information required by the repository template, although it uses…

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the equivalence risks of the refactor: the terminateHeaders(true) call at HttpResponse.h:611 sits under an HTTP_WROTE_TRANSFER_ENCODING_HEADER guard, so it cannot emit a second Transfer-Encoding line; the second writeMark() inside terminateHeaders() on the Content-Length path is a no-op via HTTP_WROTE_DATE_HEADER, so Date still precedes Content-Length; and uws_res_prepare_for_sendfile runs inside the corker callback, so Super::write("\r\n") lands in the same cork buffer getSendBuffer(2) used.

Extended reasoning...

The change is a refactor of the vendored uWebSockets HttpResponse.h and the uws_res_prepare_for_sendfile C shim, consolidating eleven hand-inlined Date/Transfer-Encoding/blank-line sequences into one terminateHeaders(bool chunked) helper, with no tests added. It touches HTTP/1 response framing (which headers are emitted and in what order) but no auth, crypto, or input-parsing surface. Each replaced site was compared against its old sequence and the chunked/non-chunked argument matches what the old code wrote at every site; the one inline finding is an extra send() on the uncorked Content-Length end path rather than a wire-format difference.

Comment thread packages/bun-uws/src/HttpResponse.h Outdated
…eaders()

Add HttpResponse::writeOwnedHeaders(chunked), which writes the Date header
(unless the caller wrote one) and the Transfer-Encoding: chunked header
for a chunked body (unless the caller wrote one), and terminateHeaders(),
which writes them and then the blank line. Every path in HttpResponse.h
that closed the header section with its own writeMark() + CRLF block now
calls terminateHeaders(), and so does uws_res_prepare_for_sendfile. The
Content-Length path of internalEnd() calls writeOwnedHeaders() and keeps
its blank line in the same write as the Content-Length line, so an
uncorked end stays one send(). uws_res_end_without_body stays as it is:
it writes no Date today.

Wire output is unchanged on every path.
@robobun
robobun force-pushed the robobun/2e7ec37d/uws-terminate-headers branch from 81f8803 to c5b9d81 Compare September 24, 2026 00:52

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

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.

1 participant