Repository navigation
Conversation
WalkthroughServer and TLS socket resume logic now preserves pauses established by connection handlers. New fixtures and parameterized tests verify paused state, backpressure behavior, and complete data delivery after resuming. ChangesSocket pause preservation
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Reproduced on main: Fix: gate the post-emit Scoped down from the first pass (which removed the CI on 07c1bfd (build 77839) is green for this diff. The Ready for review. |
|
Updated 9:30 AM PT - Jul 22nd, 2026
❌ @robobun, your commit 07c1bfd has 3 failures in
🧪 To try this PR locally: bunx bun-pr 35108That installs a local version of the PR into your bun-35108 --bun |
|
Found 3 issues this PR may fix:
🤖 Generated with Claude Code |
|
Verified the three suggested issues against this change on
None of the three are closed by this PR. The bug this PR fixes is the accepted-socket face via |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
On the duplicate candidates:
|
There was a problem hiding this comment.
I didn't find bugs, but this changes streaming semantics for every accepted net/tls server socket (drops the post-emit resume() and adds read(0) after push(null)), and two existing tests already had to be adjusted to compensate — worth a human look given the blast radius and the overlap with #34285.
What was reviewed:
onconnection/ServerHandlers.handshake: confirmed Node'sonconnectionandonServerSocketSecureleavereadableFlowingatnull; the client-sideconnectpath here already usesread(0), so this brings the server side in line.SocketEmitEndNTread(0): matches Node'sonStreamRead; checked it's inside the!self[kended]guard so it can't double-fire.- The two updated tests: the added
resume()matches what Node requires forserver.close()to complete when the handler never reads.
Extended reasoning...
Overview
Three changes in src/js/node/net.ts: (1) onconnection no longer calls _socket.resume() after emit('connection'), (2) the TLS ServerHandlers.handshake sibling drops its else self.resume(), and (3) SocketEmitEndNT adds self.read(0) after push(null) so 'end' fires on a never-flowed empty-buffer stream. A new subprocess fixture + test in node-net.test.ts covers both the pause-in-handler and buffer-early-bytes cases; two existing tests (node-tls-connect ECONNRESET, node-http-connect backpressure) add an explicit resume() because they previously relied on the auto-resume to drain unread bytes so server.close() could complete.
Security risks
None. This is stream flow-control semantics only; no auth, crypto, or untrusted-input parsing is touched.
Level of scrutiny
High. node:net is production-critical and this is a behavioral change on every accepted TCP and TLS server socket. Removing an unconditional resume() means any consumer that was implicitly relying on flowing mode (without attaching 'data' or calling resume()/pipe()) will now buffer instead of discard — which is the Node-correct behavior, but the fact that two in-tree tests had to be patched shows the old behavior was load-bearing in places. The read(0) in SocketEmitEndNT is a second behavior change (when 'end' fires) that runs on every socket EOF.
Other factors
The PR is well-researched (cites Node's lib/net.js line, verified the two adjusted tests hang in Node the same way, ran 713 test-{net,tls,http,https,http2,cluster,stream}-* parallel tests). The new fixture is thorough and polls conditions rather than sleeping. However, #34285 carries a broader version of this fix that also touches poll_ref/unref semantics — a maintainer should decide whether to land this scoped subset now or coordinate with that PR. Given the blast radius across node:http/node:http2/node:tls server paths and the existence of a competing broader PR, this should get human sign-off.
onconnection called _socket.resume() unconditionally after emit('connection'),
which overrode a pause() made inside the handler: readableFlowing went from
false back to true with no 'data' listener, inbound bytes were emitted into
the void, the kernel receive buffer drained, and the writer saw 'drain' (and
bytesWritten reported success) against a peer that would receive nothing after
resume(). Same for the TLS handshake handler's else-resume.
Gate the post-emit resume() on readableFlowing === null so a handler that
paused or attached 'data'/'readable' is honored. A handler that left the
stream untouched still gets resumed so a write-only handler's peer-close
tears the socket down; Node instead leaves readableFlowing null there and
relies on libuv's UV_EOF readStop to release the loop, which requires
accepted sockets to hold the loop on their own (not yet the case here).
cc7a7fa to
9a3578b
Compare
Sibling of the net.createServer fixture for the ServerHandlers.handshake gate.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@src/js/node/net.ts`:
- Around line 1068-1075: Shorten the comments to three lines or fewer: in
src/js/node/net.ts lines 1068-1075, retain only the durable readable-flow-state
invariant; in test/js/node/net/net-server-accepted-socket-pause-fixture.js lines
2-8, retain only the pause-preservation invariant; in
test/js/node/net/tls-server-accepted-socket-pause-fixture.js lines 2-5, condense
the TLS explanation; and in test/js/node/net/node-net.test.ts lines 1136-1142,
retain only the subprocess-isolation and pause-preservation rationale.
In `@test/js/node/net/net-server-accepted-socket-pause-fixture.js`:
- Around line 12-17: Bound the waitFor polling in
test/js/node/net/net-server-accepted-socket-pause-fixture.js:12-17 and
test/js/node/net/tls-server-accepted-socket-pause-fixture.js:13-18 with a
maximum turn count that rejects using a targeted error; update the initial wait
in the net fixture to report a missing 'connection' event and the TLS fixture to
report a missing 'secureConnection' event.
- Around line 36-59: Extract descriptive shared constants for chunk size,
maximum writes, settling poll limit, and delivery poll limit in
test/js/node/net/net-server-accepted-socket-pause-fixture.js:36-59, then use
them in the write loop and both waitFor conditions. Apply the equivalent named
workload and polling constants in
test/js/node/net/tls-server-accepted-socket-pause-fixture.js:38-58; preserve the
existing limits and behavior.
In `@test/js/node/net/node-net.test.ts`:
- Around line 1159-1161: Remove the exact-empty stderr assertion from this test,
while retaining stderr in the combined assertion or failure diagnostics so
benign native/ASAN output does not fail the test. Keep the existing stdout and
exitCode behavioral assertions unchanged.
🪄 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: b392253e-d108-4424-816a-4117cfa07804
📒 Files selected for processing (4)
src/js/node/net.tstest/js/node/net/net-server-accepted-socket-pause-fixture.jstest/js/node/net/node-net.test.tstest/js/node/net/tls-server-accepted-socket-pause-fixture.js
There was a problem hiding this comment.
LGTM — the readableFlowing === null gate is a strict narrowing of when the post-emit resume() fires, and both sibling paths (TCP onconnection + TLS ServerHandlers.handshake) are now covered by the describe.each fixtures.
What was reviewed
- Traced the gate against all three handler-touched states (
pause()→false,'data'→true,'readable'→false) and the untouchednullcase; onlynullstill resumes, matching the documented rationale. - Confirmed
pauseOnConnectinteraction is unchanged (line 1031 setsfalsebefore emit;!pauseOnConnectstill guards independently). - TLS fixture key paths (
test/js/node/test/fixtures/keys/agent1-*.pem) exist;expect(stderr).toBe("")matches 5+ neighboring subprocess tests in the same file.
Extended reasoning...
Overview
Two-line logic change in src/js/node/net.ts: the unconditional _socket.resume() after emit('connection') (and its TLS twin after emit('secureConnection')/'secureConnect') is now gated on readableFlowing === null. Two new subprocess fixtures (net-server-accepted-socket-pause-fixture.js, tls-server-accepted-socket-pause-fixture.js) and a describe.each in node-net.test.ts assert that a pause() inside the handler leaves readableFlowing at false, backpressure holds, and every byte is delivered after resume().
Security risks
None. This is stream-flow-state bookkeeping in the Node compat layer; no input parsing, auth, crypto, or trust-boundary changes.
Level of scrutiny
Medium — node:net accept-path semantics are subtle and there are overlapping open PRs (#34285, #35006) reworking the same area. But the change here is a strict narrowing: resume() can only be skipped relative to before, and only when the handler already touched flow state (which is exactly the bug case). The null-still-resumes behavior is unchanged and its rationale (write-only handler + peer-close teardown, pending the per-socket poll_ref rework) is documented in the inline comment. The author already scoped down from removing resume() entirely after CI showed that regressed write-only handlers.
Other factors
- My prior review nit (missing TLS coverage for the
ServerHandlers.handshakegate) was addressed in 4e80eb0 and the thread is resolved. - CodeRabbit's two remaining unresolved inline comments are stylistic: the magic-numbers-in-fixture one was declined with a stated rationale (each value appears once with an adjacent comment); the
expect(stderr).toBe("")one matches the exact pattern used by five other subprocess tests in the same file, so accepting it here would diverge from local convention. - robobun reports CI green on 9a3578b with only an unrelated
bun-install-proxy.test.tsflake hitting neighboring builds. - Fixtures use bounded polling for the bug-distinguishing waits and were verified to fail on main and match Node.
…xture 'drain' means the Writable buffer fell below HWM, which happens when the kernel send buffer accepts another chunk; its capacity varies by OS (darwin-aarch64 CI showed 1, linux 0). The distinguishing observables are readableFlowing and whether bytes are delivered after resume().
|
Closing: this was fixed on main by #32630, which landed a few hours after this PR was opened. onconnection in net.ts now kicks off reading with |
What
onconnectioncalls_socket.resume()unconditionally afteremit("connection", _socket). When the handler calleds.pause(), that flippedreadableFlowingfromfalseback totruewith no'data'listener attached: inbound bytes were emitted into the void, the kernel receive buffer drained, and the writer saw'drain'/bytesWrittenprogress against a paused reader that received nothing afterresume().Repro
Fix
Gate the post-emit
resume()on_socket.readableFlowing === null(the handler left the stream untouched). A handler that paused (false) or attached'data'/'readable'(true/false) is honored. Same forServerHandlers.handshake.A handler that left the stream untouched still resumes the way it did before, so a write-only handler's peer-close still tears the accepted socket down. (Node instead leaves
readableFlowingatnullthere and relies on libuv's UV_EOF readStop to release the loop; matching that would require accepted sockets to hold the loop on their own.)Verification
Fails on main (
flowing true,drainsWhilePaused 1,delivered false), passes with this change and matches Node line for line.Related: #35006 and #34285 both cover the full
readableFlowing === nullaccept semantics with the accompanying per-socketpoll_refrework. This PR is the scoped fix for the in-handlerpause()case only; #35006 subsumes it if that lands first.no test proof · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/net/node-net.test.ts