node:net: route throws from 'data'/'connection' listeners to uncaughtException and keep the socket alive - #35347
node:net: route throws from 'data'/'connection' listeners to uncaughtException and keep the socket alive#35347robobun wants to merge 1 commit into
Conversation
…Exception and keep the socket alive
An exception thrown from a user 'data' listener on a net.Socket, or from a
net.Server 'connection' listener, unwound into the native socket dispatch
which routes a throwing handler to the socket handler table's error entry.
That routing exists for transport failures (ECONNRESET, write errors) but was
misclassifying a programming error in user code as a socket error: the throw
surfaced as socket.emit('error', err) and the connection was torn down, or
was silently dropped when the error handler returned early. Node surfaces the
throw as process.on('uncaughtException') and leaves the socket reading, so
the next chunk is still delivered.
Wrap Readable.push() in the three data handler tables and the 'connection'
emit in onconnection with a try/catch that reports the throw via reportError
and leaves the socket alone.
This surfaces a second bug it had been masking: uws_res_end_without_body
wrote a Connection: close header and CRLF terminator even after res.write()
had started the body, so ServerResponse.destroy() on an in-flight chunked
response injected header bytes into the body; the client parse error that
produced used to be swallowed by the same misrouting. Gate those writes on
HTTP_WRITE_CALLED so an abort after the body started emits nothing.
|
Warning Review limit reached
Next review available in: 6 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
|
Updated 8:36 PM PT - Jul 23rd, 2026
❌ @robobun, your commit 73454fa has 1 failures in
🧪 To try this PR locally: bunx bun-pr 35347That installs a local version of the PR into your bun-35347 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
On #19789: the |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate; intentional overlap spelled out in the Related section of the description:
|
Fixes #29761
Problem
An exception thrown from a user
'data'listener on anet.Socket, or from anet.Server'connection'listener, unwinds into the native socket dispatch, which catches it and routes it to the socket handler table'serrorentry. That entry exists for transport failures (ECONNRESET, fatal write), so a programming error in a user listener was misclassified as a socket error: it surfaced assocket.emit('error', err)and the connection was torn down, or was silently dropped when the error handler returned early, so the ubiquitoussock.on('error', () => {})hid it. Node surfaces the throw asprocess.on('uncaughtException')and leaves the socket reading, so the next chunk is still delivered.Same for a throw inside the
createServer(handler)connection handler: even with no error listener the accepted connection is closed where Node keeps it established.Fix
Wrap
Readable.push()in the threedatahandler tables (SocketHandlers/ServerHandlers/SocketHandlers2) and the'connection'emit inonconnectionwith a try/catch that reports the throw viareportErrorand leaves the socket alone. The native error routing still applies to actual transport failures because those do not flow throughpush(). The publicBun.listen/Bun.connectAPI is unchanged.uws_res_end_without_bodyguardThe correct routing surfaces a second bug the misrouting was masking:
uws_res_end_without_bodywrote aConnection: closeheader and the header-terminating CRLF even afterres.write()had started the body, soServerResponse.destroy()on an in-flight chunked response injectedConnection: close\r\n\r\ninto the middle of the body. The client then parses that asHPE_INVALID_CHUNK_SIZE, andreq.emit('error', parseErr)with no listener rethrows; previously that rethrow was itself swallowed by the same native catch. Gate those writes onHTTP_WRITE_CALLEDso an abort after the body started emits nothing (Node sends nothing extra on destroy). This is theuws_res_end_without_bodyhunk of #32036.Verification
test/js/node/net/node-net.test.ts: four subprocess tests covering server-side'data'listener throw, client-side'data'listener throw,'connection'listener throw (each reachesuncaughtException, no socket'error'fires, the next chunk is delivered,close(hadError)is false), and the no-handler case (process exits 1 with the thrown error on stderr). Expected outputs taken verbatim from Node v26.3.0; all four fail on main.test/js/node/http/node-http.test.ts: destroying a chunked response mid-stream writes exactly the one chunk frame with no appended header bytes. Fails on main.Regression sweeps against this branch, failures match main:
test/js/node/test/parallel/test-http-*.js: 372/377 pass (5 pre-existing, includingtest-http-abort-client/test-http-client-aborted-event/test-http-catch-uncaughtexception/test-http-server-capture-rejectionswhich all pass)test/js/node/test/parallel/test-net-*.js: 137/140 pass (3 pre-existing)test/js/node/net/node-net.test.ts,test/js/node/http/node-http.test.ts,test/js/node/tls/node-tls-{server,connect}.test.ts,test/js/bun/http/serve.test.ts: no new failuresScope: TLS
'secureConnection'emit intentionally excludedThe SSL sibling of the
'connection'emit,server.emit("secureConnection", self)inServerHandlers.handshake, is left unwrapped. Wrapping it regressestest/js/node/test/parallel/test-tls-close-error.js: Bun currently firessecureConnectionfor a client whose verify-reject will tear the connection down (Node does not; the test's handler iscommon.mustNotCall()), and that assertion throw is today silently routed through the native error handler. Surfacing it asuncaughtExceptionfails the test. That is a pre-existing bug in when Bun firessecureConnection, tracked separately. The TLS'data'path is covered: accepted TLS sockets useServerHandlers.data, which is wrapped viapushDataToSocket, and a throwing TLS'data'listener now matches Node.Related
'connect'/'ready'/'connection'emit groups but explicitly deferred the'data'path because it unmasked theuws_res_end_without_bodymid-body injection above. This PR covers the'data'and'connection'cases and includes the guard that blocked it.socket.terminate()on a caught throw; Node leaves the socket alive and delivers the next chunk, so that teardown is the wrong semantics for this bug.uws_res_end_without_bodyhunk that the net change depends on.