Skip to content

Bun.serve: Date/Content-Length on parser-tier 4xx/5xx; 414 for oversize URI; 501 for unknown methods - #35299

Closed
robobun wants to merge 4 commits into
mainfrom
farm/dd7aa830/serve-parser-error-responses
Closed

robobun wants to merge 4 commits into
mainfrom
farm/dd7aa830/serve-parser-error-responses

Conversation

@robobun

@robobun robobun commented Jul 23, 2026 •

Copy link
Copy Markdown
Collaborator

What

Three defects in the Bun.serve parser-rejection path, all observable with a raw TCP client:

import net from "node:net";
const server = Bun.serve({ port: 0, hostname: "127.0.0.1", development: false, fetch() { return new Response("ok\n"); } });
const send = b => new Promise(res => { let o = ""; const s = net.connect(server.port, "127.0.0.1", () => s.write(b));
  s.on("data", d => o += d.toString("latin1")); s.on("close", () => res(o)); s.on("error", () => res(o)); });

// 1) parser 4xx/5xx have no Date / Content-Length
await send("GET / HTTP/1.1\r\nHost: x\r\nNo-Colon-Line\r\n\r\n");
// -> "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"

// 2) 64 KiB request-target answered 431, not 414
await send("GET /" + "a".repeat(65536) + " HTTP/1.1\r\nHost: x\r\n\r\n");
// -> "HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n"

// 3) unknown method tokens close with zero response bytes
await send("FROB / HTTP/1.1\r\nHost: x\r\n\r\n");
// -> "" (silent close; same for "BREW", "get", "GETS")

Cause

  • httpErrorResponses[] (packages/bun-uws/src/HttpErrors.h) held full responses containing only the status line and Connection: close; every handler-path response writes Date (RFC 9110 6.6.1: MUST on any response from a server with a clock) and Content-Length.
  • consumeRequestLine and the fallback-buffer overflow paths in HttpParser.h reported any request-head overflow as HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, with no way to tell a request-target overflow (414, RFC 9110 15.5.15) from header-field overflow (431).
  • HttpContext.h called us_socket_close() with no write when router->route() missed. Bun.serve's any() registration expands "*" to the fixed supportedHttpMethods list, so the ANY_METHOD_TOKEN fallback node carries no handler and a method token outside that list (FROB, lowercase get, GETS, BREW) routes nowhere.

Fix

  • httpErrorResponses[] now holds just the status lines; a new writeHttpErrorAndClose() appends Connection: close, Content-Length: 0, and the loop's cached Date before shutdown/close. Both the parser-error path and the route-miss path use it.
  • The parser classifies an oversize request-target as 414: after a successful parse the request-target length is checked against the limit; on short-read overflow, a request-line that has not terminated (no LF in the buffered window) is 414, otherwise 431. The HttpParserError reported to onClientError is unchanged (HEADER_FIELDS_TOO_LARGE), matching Node/llhttp which only has HPE_HEADER_OVERFLOW; node:http's default 431 response is therefore unchanged.
  • A router miss writes HTTP/1.1 501 Not Implemented (RFC 9110 9.1) with the same headers before closing.

Verification

New describe("parser-tier error responses") block in test/js/bun/http/serve.test.ts:

  • 400/431/505 carry Date (IMF-fixdate) and Content-Length: 0, body is empty.
  • Oversize request-target (single write and trickled) -> 414; oversize header field -> still 431.
  • FROB/get/BREW/GETS -> 501 with Date/CL; PROPFIND/PURGE -> dispatched to fetch() (200).

All three tests fail on main and pass with this change. test/js/bun/http/request-smuggling.test.ts and the node:http test-http-header-overflow.js / test-http-server-client-error.js / test-http-server-destroy-socket-on-client-error.js parallel tests are unchanged.


no test proof · iteration 1 · 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

Fixes #6556

Related: #21566 (the Bun.serve side no longer closes silently; requests now get 501 instead, though custom methods are still not dispatched to fetch()), #27512 (that 413 is the maxRequestBodySize early rejection in src/runtime/server/mod.rs, which uses write_status + end_without_body rather than the parser-error table touched here; not changed by this PR).

…ze URI; 501 for unknown methods

The request-parser rejection path wrote only a status line and
Connection: close. RFC 9110 6.6.1 says an origin server with a clock
MUST send Date on every response, and the absence of Content-Length
left the body framing to connection close; the handler path already
sends both. The status-line table is now just status lines, and
writeHttpErrorAndClose() appends Connection: close, Content-Length: 0
and the loop's cached Date before shutting the socket.

An oversize request-target was answered 431 (Request Header Fields Too
Large). 431 is for header fields; 414 is the status for a
request-target that alone exceeds the server's limit (RFC 9110
15.5.15). The parser now distinguishes the two: after a parse the
request-target length is compared to the limit, and on a short-read
overflow a request-line that has not yet terminated (no LF in the
buffered bytes) is classified 414. The node:http clientError code is
unchanged (llhttp reports HPE_HEADER_OVERFLOW either way).

A syntactically valid method token outside the router's method table
(lowercase "get", "FROB", "BREW", "GETS") closed the connection
with zero response bytes: Bun.serve expands the wildcard handler to
supportedHttpMethods, so the ANY fallback node has no handler and the
route miss went straight to us_socket_close. A miss now writes 501 Not
Implemented (RFC 9110 9.1) with the same Date/Content-Length headers
before closing.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 741cf186-0ca5-4862-898c-ec2b7f3d0d9c

📥 Commits

Reviewing files that changed from the base of the PR and between f44a618 and ccb5e81.

📒 Files selected for processing (1)
  • test/js/bun/http/serve.test.ts

Walkthrough

Changes

HTTP error response handling

Layer / File(s) Summary
Error status contracts and response formatting
packages/bun-uws/src/HttpErrors.h, packages/bun-uws/src/HttpContext.h
Adds 414 and 501 statuses and centralizes zero-length error responses with Date, Content-Length, and Connection: close headers.
Parser overflow classification
packages/bun-uws/src/HttpParser.h
Distinguishes oversized request targets returning 414 from oversized header fields returning 431 across direct and fallback parsing paths.
Socket error response wiring
packages/bun-uws/src/HttpContext.h
Uses the shared error-response helper for parser failures and routed methods without handlers.
Parser-tier response tests
test/js/bun/http/serve.test.ts
Adds raw-socket coverage for error headers, 414/431 classification, 501 responses, and custom method dispatch.

Possibly related PRs

  • oven-sh/bun#32488: Both modify uWS HTTP error and parser plumbing for status mapping and socket closure.
  • oven-sh/bun#34296: Both modify parser-detected error handling and HTTP connection state management.

Suggested reviewers: cirospaciari

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes parser-tier headers and URI-length classification, which are unrelated to #6556. Move the parser-error header and 414/431 changes into a separate PR or link the corresponding issue(s).
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #6556 by replacing silent closes for unsupported methods with a 501 response.
Title check ✅ Passed The title is concise and accurately summarizes the main parser-response and method-handling changes.
Description check ✅ Passed The description clearly explains the changes, rationale, and verification steps, covering the template's required intent.

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

@robobun

robobun commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced with the raw-socket script (exit 86 on canary). With this change:

ok  "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\nDate: ..."  400
ok  "HTTP/1.1 414 URI Too Long\r\nConnection: close\r\nContent-Length: 0\r\nDate: ..."  (64k target)
ok  "HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\nContent-Length: 0\r\nDate: ..."  505
method FROB: "HTTP/1.1 501 Not Implemented\r\nConnection"
method get:  "HTTP/1.1 501 Not Implemented\r\nConnection"
method BREW: "HTTP/1.1 501 Not Implemented\r\nConnection"
exit: 0

Tests added to test/js/bun/http/serve.test.ts (parser-tier error responses); all three fail on main, pass here.

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. 413 HTTP Responses do not include a Date header #27512 - Missing Date header on parser-tier error responses (413); this PR adds Date and Content-Length to all pre-handler 4xx/5xx responses
  2. Custom HTTP methods via Bun.serve() #6556 - Custom/unknown HTTP methods cause "Empty reply from server" (silent connection close); this PR returns 501 Not Implemented instead
  3. Non-standard and broken handling of HTTP methods in both fetch and Bun.serve #21566 - Non-standard HTTP method handling in Bun.serve (server-side: unknown methods close connection without response); this PR fixes the server-side half by returning 501

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #27512
Fixes #6556
Fixes #21566

🤖 Generated with Claude Code

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bun-uws/src/HttpParser.h`:
- Line 1561: Update the overflow handling around headOverflowStatus so the scan
uses the bounded concatenation of fallback and data: scan fallback first, then
only the valid data prefix that fits within maxFallbackSize, bounded by length.
Avoid reading beyond buffered bytes and preserve detection of newlines already
present in fallback.

In `@test/js/bun/http/serve.test.ts`:
- Around line 3697-3699: Update the rawRequest error handler so socket or
connection errors reject the helper promise instead of resolving it. Preserve
normal resolution for successful responses, and propagate the original transport
error through the rejection path.
- Around line 3760-3766: Make the fragmented request setup in the rawRequest
test deterministic by introducing an explicit parser boundary or other
observable synchronization barrier between the 10,000-byte prefix, big, and HTTP
headers. Ensure the test reliably exercises the fallback-buffer overflow path
rather than allowing write/flush coalescing to produce a single read.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 378d04ff-e31e-4361-ae6e-4f1444090ab6

📥 Commits

Reviewing files that changed from the base of the PR and between 892b1da and f44a618.

📒 Files selected for processing (4)
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpErrors.h
  • packages/bun-uws/src/HttpParser.h
  • test/js/bun/http/serve.test.ts

Comment thread packages/bun-uws/src/HttpParser.h
Comment thread test/js/bun/http/serve.test.ts
Comment thread test/js/bun/http/serve.test.ts
Comment thread test/js/bun/http/serve.test.ts Outdated
…headOverflowStatus

Client and server share one event loop in-process, so back-to-back
conn.write() calls all queued before the server's onData fired and the
trickled request arrived as one read, taking the same post-parse
classifier as the single-write case. Yielding between writes lets each
piece reach onData separately, so the first two land in the fallback
buffer and the third fills it, and headOverflowStatus() is what returns
414. Verified by breaking the helper to always return 431: trickled now
fails while uriOnce still passes.

@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.

The ccb5e81 fix addresses the trickled-write coverage gap I flagged; no further issues found. Deferring to a maintainer since this changes user-visible Bun.serve behavior (501 for unrouted methods, 414 vs 431 classification) in the core uWS parser/context path.

What was reviewed:

  • writeHttpErrorAndClose: buffer sizing (worst case 123 < 160), %.29s bounds the LoopData date read, and LoopData() initializes date in its ctor.
  • Router-miss 501: node:http registers on ANY_METHOD_TOKEN (its useStrictMethodValidation skips the supportedHttpMethods expansion in onHttp) and rejects unknown methods at parse time, so its clientError path is unaffected; Bun.serve always registers a /* handler on every supported method.
  • headOverflowStatus(data, maxFallbackSize) at HttpParser.h:1561 is bounded (length >= maxFallbackSize in that branch, fallback already cleared).
Extended reasoning...

Overview

Touches packages/bun-uws/src/{HttpErrors,HttpContext,HttpParser}.h and adds a describe block to test/js/bun/http/serve.test.ts. Three behavioral changes to the Bun.serve parser-rejection path: (1) parser-tier 4xx/5xx responses now carry Date and Content-Length: 0 via a new writeHttpErrorAndClose helper; (2) request-target overflow is classified as 414 (both post-parse and via a new headOverflowStatus LF scan for the fallback-buffer paths); (3) a router miss now writes 501 instead of silently closing.

Security risks

This is HTTP protocol handling on untrusted input. Checked: the snprintf into char buf[160] cannot overflow (longest status line is 44 bytes; total worst case ~123); %.29s bounds the date read and LoopData::date[32] is initialized in the ctor. The memchr in headOverflowStatus at both call sites reads only within valid bytes (the second call site is in the length >= maxFallbackSize branch with fallback already cleared, as robobun noted). The HttpParserError reported to node:http clientError is unchanged, so the node compat default 431 response and HPE_HEADER_OVERFLOW mapping are preserved.

Level of scrutiny

High — this is the core uWS HTTP server path shared by Bun.serve and node:http, and it changes wire-visible behavior. The 501-on-router-miss is a design decision: it improves on the current silent close but does not dispatch custom methods to fetch() (which #21566 asks for), so a maintainer should confirm 501 is the intended intermediate step. The 414/431 heuristic (no LF in the buffered window ⇒ request-line not terminated ⇒ 414) is reasonable but is a classification choice worth a human eye.

Other factors

My earlier inline comment (the trickled test not exercising headOverflowStatus) was fixed in ccb5e81 with an await Bun.sleep(0) yield between writes, and the author verified by mutation-testing the helper. All CodeRabbit threads are resolved (two withdrawn as not-a-bug, one addressed by the same commit). Tests cover 400/431/505 headers, 414 single-read + trickled, 431 for oversize header field, and 501 for four unknown method tokens plus dispatch of two known extension methods. The ref-count / cork behavior of the router-miss path is unchanged from the previous us_socket_close-only version.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-23 and it conflicts with main. This is not a judgment on the fix itself. The linked issue (#6556) stays open. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

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.

Custom HTTP methods via Bun.serve()

2 participants