Skip to content

fix: let clientError handler send response before closing socket#28642

Open
robobun wants to merge 1 commit intomainfrom
farm/27c20043/fix-client-error-socket-destroy
Open

fix: let clientError handler send response before closing socket#28642
robobun wants to merge 1 commit intomainfrom
farm/27c20043/fix-client-error-socket-destroy

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Mar 29, 2026

Fixes #28641

Problem

When http.createServer receives a request with headers exceeding the size limit, the clientError event fires but uWebSockets immediately writes a default error response, shuts down the socket, and closes it. This prevents user code from sending a custom response (e.g. 431 Header Too Large).

Expected (Node.js): The clientError handler receives a writable socket, sends a custom response, and closes the socket itself.

Actual (Bun): The socket is destroyed before the handler's socket.end(...) can deliver data.

Root Cause

In packages/bun-uws/src/HttpContext.h, after calling the onClientError callback, the code unconditionally:

  1. Writes a default error response
  2. Shuts down the socket
  3. Closes the socket

Additionally, the NodeHTTPServerSocket created in the error path never had streaming enabled, so its _write() method was a no-op (it checks handle.ondrain before writing).

Fix

  1. HttpContext.h: When an onClientError handler is registered, call it and skip the automatic write/shutdown/close. The handler takes responsibility for the socket lifecycle. Uncork the socket so any data written by the handler gets flushed.

  2. _http_server.ts: Enable streaming on the NodeHTTPServerSocket created in the error path so _write() can send data. When no clientError listener is attached, send a default 400 response matching Node.js behavior.

Verification

USE_SYSTEM_BUN=1 bun test test/regression/issue/28641.test.ts  → 2 FAIL
bun bd test test/regression/issue/28641.test.ts                → 2 PASS

@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Mar 29, 2026

Updated 2:46 AM PT - Apr 6th, 2026

❌ Your commit a4bb934b has 9 failures in Build #43929 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28642

That installs a local version of the PR into your bun-28642 executable, so you can run:

bun-28642 --bun

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

C++ and JS HTTP client-error flows were changed: the native path now invokes a user clientError handler and uncorks/returns the socket instead of sending a canned response; the JS server reuses or creates a socket, enables streaming, emits "connection" only for new sockets, and falls back to Node-like 431/400 handling when no clientError handler sends a response. Two regression tests were added.

Changes

Cohort / File(s) Summary
Native HTTP error path
packages/bun-uws/src/HttpContext.h
When result.httpErrorStatusCode() is nonzero and httpContextData->onClientError exists, call the handler, uncork the socket via ((AsyncSocket<SSL>*) s)->uncork(), return the socket, and bypass the previous canned-response/shutdown/close fallback.
JS HTTP server error handling
src/js/node/_http_server.ts
Attempt to reuse an existing socket.duplex or create a new NodeHTTPServerSocket, always call nodeSocket[kEnableStreaming](true), emit "connection" only for newly created sockets, emit clientError with (err, nodeSocket), and when no response was sent perform Node-like default handling (write 431 for HPE_HEADER_OVERFLOW or 400 otherwise) or destroy the socket; removed the prior nodeSocket.emit("error", err) listener-count path.
Regression tests
test/regression/issue/28641.test.ts
Added two tests that spawn child TCP clients sending ~128KB headers: one verifies a server.on("clientError") handler can write HTTP/1.1 431 Header Too Large and the other verifies the default behavior returns 431 when no handler is present.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: enabling clientError handlers to send responses before socket closure, which is the primary objective of the PR.
Description check ✅ Passed The description comprehensively covers the problem, root cause, implemented fixes, and verification steps, matching the template structure with detailed technical context.
Linked Issues check ✅ Passed The code changes directly address issue #28641 requirements: enabling writable sockets in clientError handlers, supporting custom responses, and matching Node.js behavior for header overflow scenarios.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing clientError handling: HttpContext.h controls handler invocation, _http_server.ts manages streaming/response logic, and 28641.test.ts validates the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Comment thread src/js/node/_http_server.ts Outdated
Comment thread packages/bun-uws/src/HttpContext.h Outdated
Comment thread test/regression/issue/28641.test.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/js/node/_http_server.ts`:
- Around line 810-815: The clientError handler currently sends a hardcoded "400
Bad Request" and uses bytesWritten to decide whether to write then destroy the
socket; update this to send "431 Request Header Fields Too Large" when the error
code is "HPE_HEADER_OVERFLOW" and use the socket's _httpMessage?.headersSent
flag instead of nodeSocket.bytesWritten to detect if a response has already
started; also replace the write()+destroy() pattern with end() so the response
is flushed before closing (then destroy(err) only if needed). Locate the logic
around emit("clientError", err, nodeSocket) and modify the branch that writes
the response to implement the 431 status for HPE_HEADER_OVERFLOW, check
_httpMessage?.headersSent, and call end() for graceful close.
- Around line 807-810: The error path currently always creates a new
NodeHTTPServerSocket and re-emits "connection", causing duplicate connection
events and state loss; change it to reuse the wrapper stored on handle.duplex
when present instead of unconditionally allocating a new NodeHTTPServerSocket:
look up handle.duplex before constructing NodeHTTPServerSocket, set nodeSocket
to that existing wrapper if found, only call self.emit("connection", nodeSocket)
when isSocketNew (and !reachedRequestsLimit) like the normal request path, and
then continue to call nodeSocket[kEnableStreaming](true) and
self.emit("clientError", err, nodeSocket) so the same wrapper and its
listeners/state are preserved.

In `@test/regression/issue/28641.test.ts`:
- Around line 32-41: The embedded client fixture only closes the server on the
'end' and 'error' events; add a single guarded completion helper (e.g., function
finish(reason) { if (finished) return; finished = true;
process.stdout.write(...); server.close(); }) and call it from the existing
client.on('end', ...), client.on('error', ...) handlers and also from
client.on('close', ...) so socket teardown cannot leave the subprocess hanging;
reference the client.on('end'), client.on('error'), client.on('close'), the
`data` accumulator and `server.close()`/process.stdout.write calls when making
the change and apply the same pattern to the other embedded script mentioned.
🪄 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: 804fba1b-787d-471e-a714-f3223fa9b075

📥 Commits

Reviewing files that changed from the base of the PR and between 3051c32 and dc7539d.

📒 Files selected for processing (3)
  • packages/bun-uws/src/HttpContext.h
  • src/js/node/_http_server.ts
  • test/regression/issue/28641.test.ts

Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread test/regression/issue/28641.test.ts Outdated
Comment thread test/regression/issue/28641.test.ts
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — all previously raised issues have been addressed.

Extended reasoning...

Overview

The PR fixes a real Node.js compatibility gap: when clientError fires for oversized headers, Bun was immediately writing a default response and destroying the socket before user handlers could respond. The fix spans HttpContext.h (defer socket lifecycle to the JS handler, uncork after) and _http_server.ts (enable streaming on the error socket, reuse existing wrapper via handle.duplex, emit 431 for HPE_HEADER_OVERFLOW, use write()+destroy() to match Node.js semantics).

Security risks

No new attack surface introduced. The error path now correctly closes the socket after sending the response, matching Node.js behavior. The previous nodeSocket.end() call (which could leave half-open connections) was corrected to write()+destroy().

Level of scrutiny

This is a Node.js compatibility fix in HTTP server infrastructure — moderate scrutiny appropriate. The PR went through multiple rounds of review with the author actively addressing each issue found.

Other factors

All bugs found in prior review iterations have been fixed: the write/destroy semantic fix (dc7539d), the dead conditional removal in HttpContext.h (dc7539d), socket wrapper reuse via handle.duplex (c96ceb7), correct 431 status for HPE_HEADER_OVERFLOW (c96ceb7), guarded finish() helper in tests (c96ceb7), and the ASAN pipe deadlock fix via stderr: "ignore" (c0eb5d2). The remaining nit about handle.ondrain not being cleared in _destroy() is noted in the inline comment — it is structurally asymmetric but protected by C++ synchronous-close semantics and matches the existing CONNECT path behavior.

Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
@robobun robobun force-pushed the farm/27c20043/fix-client-error-socket-destroy branch from 63f0491 to 1167b09 Compare March 30, 2026 06:32
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts
@robobun robobun force-pushed the farm/27c20043/fix-client-error-socket-destroy branch 2 times, most recently from 4701557 to 8e39434 Compare March 30, 2026 08:47
Comment thread src/js/node/_http_server.ts
@robobun robobun force-pushed the farm/27c20043/fix-client-error-socket-destroy branch from 8e39434 to 6c9cebd Compare March 30, 2026 08:55
Comment thread packages/bun-uws/src/HttpContext.h
@robobun robobun force-pushed the farm/27c20043/fix-client-error-socket-destroy branch from 454f8d5 to c358f7f Compare March 30, 2026 10:38
Comment thread packages/bun-uws/src/HttpContext.h
@robobun robobun force-pushed the farm/27c20043/fix-client-error-socket-destroy branch 2 times, most recently from 22beef4 to bd88539 Compare March 30, 2026 12:34
Comment thread src/js/node/_http_server.ts
When http.createServer receives a request with headers exceeding the
size limit, the clientError event fires but uWebSockets immediately
writes a default error response, shuts down the socket, and closes it.
This prevents user code from sending a custom response (e.g. 431).

- HttpContext.h: Skip automatic write/shutdown/close when onClientError
  handler is registered. Unref and uncork socket so handler-written data
  flushes and socket doesn't keep the event loop alive.
- _http_server.ts: Enable streaming on error-path socket so writes work.
  Reuse existing socket wrapper on keep-alive connections. Send default
  400/431 response only if handler didn't write anything. Use proper 431
  for HPE_HEADER_OVERFLOW.

Fixes #28641
@robobun robobun force-pushed the farm/27c20043/fix-client-error-socket-destroy branch from fc5d69b to a4bb934 Compare April 6, 2026 06:31
Comment thread src/js/node/_http_server.ts
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.

http.createServer destroys socket instead of sending 431 response

1 participant