Repository navigation
node:net: keep accepted socket open while buffered inbound data is unread - #36332
Conversation
…read
A server-accepted socket that receives DATA + FIN before the app attaches a
reader was being destroyed one setImmediate after the FIN, with the buffered
payload still unread. Any server that hands the accepted socket to an async
pipeline (auth lookup, worker handoff, any await before .on('data')) lost the
entire request of every client that sends and then half-closes (curl,
shutdown(SHUT_WR), write-then-FIN RPC clients). No error, no 'end': the socket
was just dead with the bytes stranded.
Node has no such reclaim: a paused socket with buffered data stays open until
it is read or destroyed, and 'end' only fires once the buffer is drained. The
handle is already unref'd by finishSocketEnd, so leaving the socket open does
not pin the event loop.
Remove destroyAbandonedNT and the kReaderInterest bookkeeping that fed it.
Introduced in 85f22bb (#34598).
|
Updated 7:05 AM PT - Jul 29th, 2026
@Jarred-Sumner, your commit 427ddfb is building: |
|
Reproduced with PR: #36332 CI status: |
WalkthroughRemoves internal reader-interest tracking from Node-compatible socket teardown and server connection handling. Adds regression coverage confirming buffered data remains readable and the server socket stays open after a client writes data and sends FIN before listeners attach. ChangesSocket EOF lifecycle
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@test/js/node/net/node-net.test.ts`:
- Around line 1147-1148: Update the teardown comment near the readable-stream
check to remove references to the removed abandoned-socket teardown,
setImmediate, peer FIN, and _readableState. Keep only the durable rationale that
lifecycle notifications are allowed to settle before buffered data is checked.
- Around line 1164-1168: Update the finally cleanup block to await
server.close() before the test exits, preserving the existing socket and client
destruction calls and ensuring the server listener is fully closed before
cleanup completes.
🪄 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: 5bd858bb-7f7b-46a3-80c3-92bedd3fee44
📒 Files selected for processing (2)
src/js/node/net.tstest/js/node/net/node-net.test.ts
💤 Files with no reviewable changes (1)
- src/js/node/net.ts
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not duplicates:
|
There was a problem hiding this comment.
No issues found — the deletion is clean, finishSocketEnd already unref()s the handle so the removed reclaim wasn't needed to release the loop, and the new test fails on main / passes with the fix. Deferring because this reverts socket-lifecycle logic added by #34598 five days ago in a core compat module; worth a human confirming there was no intentional fd-reclaim motive behind destroyAbandonedNT that isn't captured in a test.
What was reviewed:
- Confirmed
kReaderInterest/destroyAbandonedNThave no remaining references and no test in #34598 depended on the reclaim firing (its two related tests only asserted it didn't fire). - Verified
finishSocketEndunrefs the native handle at src/js/node/net.ts:576, so an abandoned FIN'd socket won't pin the event loop after this change. - The bounded
_readableState.endedpoll now used in both sibling tests replaces the prior 50ms sleep (my earlier nit, addressed in 867cc6e).
Extended reasoning...
Overview
The PR deletes 27 lines from src/js/node/net.ts: the kReaderInterest symbol, the post-emit('connection') / post-emit('secureConnection') reader-interest checks, the setImmediate(destroyAbandonedNT, self) scheduled from SocketEmitEndNT, and destroyAbandonedNT itself. It adds one regression test and updates two sibling tests in the net.Server accepted-socket buffering describe block to drop stale references to the removed teardown and replace a 50ms sleep with a bounded _readableState.ended poll.
Deleted code was load-bearing?
I traced the origin to 85f22bb (#34598, merged 2026-07-24). That PR added destroyAbandonedNT alongside two tests, but both tests assert the reclaim does not fire (client sockets, setImmediate-attached readers) — nothing asserts it does fire. The bail condition self.readableLength === 0 means it only destroyed sockets that still held unread data, which matches the PR's diagnosis that the guard was inverted. Node has no equivalent path. finishSocketEnd (called immediately before the removed schedule) already socket.unref?.()s the native handle, so leaving the JS Socket alive does not pin the event loop.
Security risks
None identified. No user-controlled parsing, no auth/crypto surface. The only lifetime concern would be fd retention for accepted sockets whose app never reads them; that matches Node semantics and the fd is released when the app drops its reference or the process exits (handle is unref'd).
Level of scrutiny
Medium-high: src/js/node/net.ts is a core Node-compat module and this alters accepted-socket lifecycle at FIN time. The diff itself is a pure deletion with no new branches, so the mechanical review is straightforward — the open question is whether #34598 had an unstated reason for the reclaim (e.g., fd pressure under an accept-and-ignore workload) that a maintainer should confirm before it's removed.
Other factors
- My earlier inline nit (bare 50ms sleep left uncommented) was addressed in 867cc6e by switching to the bounded poll.
- CodeRabbit's two comments were addressed/withdrawn.
- robobun independently reproduced fail-on-main / pass-with-fix.
- Grep confirms no remaining
kReaderInterest/destroyAbandonedNTreferences anywhere insrc/.
There was a problem hiding this comment.
No blocking issues found — the deletion is clean (no orphaned kReaderInterest/destroyAbandonedNT references remain) and the new regression test pins the Node-compat behavior. Deferring to a human because this reverts part of #34598's server-socket FIN handling that landed five days ago; worth having the author of that PR confirm destroyAbandonedNT wasn't load-bearing for something the TLS suite doesn't cover.
Also checked: finishSocketEnd already unref()s the handle after peer FIN (net.ts:574-577), so leaving the buffered socket open does not pin the event loop; the TLS secureConnection post-emit reader check removed at ServerHandlers.handshake had no consumer other than the deleted destroyAbandonedNT gate.
Extended reasoning...
Overview
Pure deletion in src/js/node/net.ts (27 lines: kReaderInterest symbol, its three write sites in onconnection/ServerHandlers.handshake/SocketEmitEndNT, and the destroyAbandonedNT function), plus one new regression test and comment/poll-loop adjustments to two existing sibling tests in test/js/node/net/node-net.test.ts. The mechanism being removed was introduced in #34598 (85f22bb, 2026-07-24) and destroyed server-accepted sockets that received DATA+FIN before a reader attached — the opposite of Node's behavior, where buffered bytes stay available for a late .on('data').
Security risks
None. No auth/crypto/permissions surface; this is stream-lifecycle bookkeeping in the JS builtin layer. Removing an eager destroySoon() cannot introduce a resource leak that pins the process because finishSocketEnd already unref()s the native handle once the peer FIN arrives.
Level of scrutiny
Medium-high. node:net socket EOF handling is production-critical and this changes observable behavior for every server that accepts a write-then-half-close client. The change itself is small and well-argued (Node has no equivalent reclaim; the PR description traces the divergence to a specific commit and confirms the event-loop-pin concern is already handled by unref()), but it reverts a piece of a large maintainer-authored PR from five days ago whose commit message doesn't state why destroyAbandonedNT was added. The #34598 diff added no test that positively asserts the abandoned-socket destroy fires (only tests that it does not fire for clients / setImmediate-attached readers), so nothing in-tree obviously depended on it — but the original author should confirm.
Other factors
- Grep confirms no remaining references to
destroyAbandonedNTorkReaderInterestanywhere undersrc/. - The new test follows the exact structure of the three sibling tests in the same describe block (same cleanup pattern, same
Promise.withResolvers+ bounded-setImmediatepoll on_readableState.ended); PR verification shows it fails on main and passes with the fix, andnode-net-server/node-net-allowHalfOpensuites are unchanged. - My earlier inline comment about the bare 50ms sleep was addressed in 867cc6e by switching to the same bounded poll; the one remaining nit (poll not exiting on
client.destroyed) only affects failure-path diagnostics and is non-blocking. - Two related open PRs (#28289, #34285) were flagged as potential overlaps; the author's rebuttal that they target different layers/tree states looks correct, but it's another reason a maintainer familiar with the area should sign off.
There was a problem hiding this comment.
No bugs found, and both earlier test-hygiene findings were addressed in 867cc6e and a8a8fd5. Deferring rather than approving because this deletes destroyAbandonedNT/kReaderInterest, which was intentionally added five days ago in #34598 (a large TLS PR) — the original author should confirm nothing on the TLS accepted-socket path was relying on this reclaim.
What was reviewed
src/js/node/net.ts: confirmed all threekReaderInterestwrite sites and theSocketEmitEndNTscheduler are removed together; no dangling references remain.finishSocketEndalreadyunref()s the handle, so leaving the socket open with buffered data does not pin the event loop.- #34598's own test additions in
node-net.test.tsassert the reclaim doesn't fire (client-side, setImmediate-attach), so they remain valid after removal. - New test's poll loops now exit on
.destroyedso a regression fails fast instead of timing out.
Extended reasoning...
Overview
This PR removes the destroyAbandonedNT mechanism and its kReaderInterest bookkeeping from src/js/node/net.ts (27 lines deleted, three sites: symbol declaration, SocketEmitEndNT scheduler, ServerHandlers.handshake post-emit check, and onconnection post-emit check plus the function itself). It adds one regression test to test/js/node/net/node-net.test.ts and adjusts two neighboring tests' comments/wait mechanisms to drop references to the removed teardown.
Security risks
None. This is socket-lifecycle timing in the Node compat layer; no auth, crypto, or input-validation surface is touched.
Level of scrutiny
Medium-high. The diff is small and pure-deletion on the source side, and the PR description gives a clear repro, root-cause trace, and Node-behavior comparison. However, the deleted code was added deliberately in #34598 (85f22bb, 2026-07-24) — a large TLS PR authored by cirospaciari with Jarred as co-author. The #34598 commit message doesn't mention destroyAbandonedNT explicitly, and the tests #34598 added to node-net.test.ts assert the reclaim doesn't fire in specific cases rather than that it does, so removal doesn't break those. But REVIEW.md's "before deleting odd-looking code, git-blame why it was written" applies: the mechanism was scoped to server-accepted sockets and also armed on the TLS secureConnection path, so there may have been a TLS test-suite scenario (one of the 14 vendored upstream tests in #34598) that motivated it. That is worth a human confirming rather than a bot approving a reversal of a five-day-old maintainer decision.
Other factors
- The bug-hunting system found no issues this run. My two prior inline findings (bare 50ms sleep → bounded poll; poll loop lacking a
.destroyedexit) were both applied. - The github-actions duplicate-PR bot flagged #28289 and #34285 as related; robobun's response distinguishes them convincingly (different tree state / different layer), but it does mean this area has multiple in-flight approaches a human should reconcile.
- The behavioral argument is sound:
destroyAbandonedNTdestroyed exactly the sockets holding unread buffered data (readableLength === 0was a bail-out), which is data loss, and Node has no equivalent.finishSocketEndalreadyunref()s the handle so keeping the socket open does not pin the loop.
There was a problem hiding this comment.
No issues found — the deletion is clean, finishSocketEnd already unref()s the handle so an unread-buffer socket won't pin the loop, and the new test proves the Node-compat gap. Deferring only because this reverts destroyAbandonedNT added deliberately in #34598 five days ago; worth a quick look from that PR's author to confirm it wasn't guarding a specific TLS abandoned-socket scenario there.
What was reviewed
kReaderInteresthad no remaining readers after the deletions (grep clean).finishSocketEndat src/js/node/net.ts:575 unrefs the handle on FIN, so removingdestroySoon()doesn't re-pin the event loop.- Both bounded poll loops now exit on
.destroyed(my earlier comments), so a regression fails fast instead of timing out.
Extended reasoning...
Overview
This PR deletes ~27 lines from src/js/node/net.ts: the destroyAbandonedNT function, the kReaderInterest symbol, and the three sites that set/read it (in SocketEmitEndNT, ServerHandlers.handshake, and onconnection). It adds one new test to test/js/node/net/node-net.test.ts and updates two existing sibling tests in the accepted-socket buffering describe block (comment cleanup + replacing a 50ms sleep with a bounded .ended poll per my earlier review).
The removed code was added in 85f22bb (#34598, merged 2026-07-24) and would destroySoon() a server-accepted socket one setImmediate after peer FIN if the app had not synchronously attached a reader — but only when readableLength > 0, i.e. it destroyed exactly the sockets holding unread inbound data. Node has no such reclaim; a paused socket with buffered data stays open until read or destroyed. I verified finishSocketEnd (src/js/node/net.ts:562-578) already unref()s the native handle on FIN, so leaving these sockets open does not pin the event loop, which was the only plausible non-compat rationale for the reclaim.
Security risks
None. This is a Node-compat lifecycle change in the JS net layer; no auth, crypto, parsing, or untrusted-input handling is touched.
Level of scrutiny
Medium-high. The diff is small and pure-deletion, and the reasoning is airtight (the removed guard's readableLength === 0 bail condition is inverted from any sensible resource-reclaim intent — it spares empty sockets and destroys the ones with data to lose). But node:net is a critical compat layer, and the deleted code was added deliberately five days ago as part of a large maintainer TLS PR (#34598). The #34598 commit message doesn't state a rationale for destroyAbandonedNT specifically, and the tests added alongside it (the two siblings this PR now updates) document around it rather than for it, so I can't rule out that it was covering a TLS-side scenario (abandoned post-handshake sockets in a vendored Node TLS test, for instance) that isn't exercised by the net suite.
Other factors
- All prior review feedback on this PR (two
claude[bot]inline comments, two CodeRabbit nits) has been addressed and resolved. grepconfirms no remaining references tokReaderInterestordestroyAbandonedNTanywhere in the tree.- robobun confirmed the new test fails on main (
destroyed: true) and passes with the fix; CI is green on all lanes that ran. - The PR correctly notes it is not a duplicate of #28289 (stale, targets pre-#34598 tree) or #34285 (different layer — initial
readableFlowingstate).
Given the change reverts a very recent deliberate addition by a maintainer, I'd rather that author or another maintainer confirm the removal doesn't regress whatever motivated it in #34598 before this lands.
What does this PR do?
A server-accepted
net.Socketthat receives DATA + FIN before the app attaches a reader was being destroyed onesetImmediateafter the FIN, with the buffered payload still unread. Any server that hands the accepted socket to an async pipeline (auth lookup, worker handoff, anyawaitbefore.on('data')) lost the entire request of every client that sends and then half-closes (curl,shutdown(SHUT_WR), write-then-FIN RPC clients). No error, no'end': the socket was just dead with the bytes stranded inreadableLength.Repro
Cause
SocketEmitEndNTscheduledsetImmediate(destroyAbandonedNT, self)on peer FIN when no reader had been engaged synchronously from the'connection'handler.destroyAbandonedNTthen calleddestroySoon()precisely whenreadableLength > 0with no listener, i.e. it destroyed exactly the sockets that still held unread inbound data (an empty-buffer FIN was spared). Introduced in 85f22bb (#34598).Node has no such reclaim: a paused socket with buffered data stays open until it is read or destroyed, and
'end'only fires after the buffer is drained. The handle is alreadyunref()'d byfinishSocketEnd, so leaving the socket open does not pin the event loop.Fix
Remove
destroyAbandonedNTand thekReaderInterestbookkeeping that fed it (27 lines deleted, no replacement). An accepted socket with buffered payload + peer FIN now stays open until the app reads (then'end'fires and the normalallowHalfOpen/autoDestroypath closes it) or destroys it, matching Node.Verification
The new test fails on main (
destroyed: true, events: ["close:false"], readableLength: 18) and passes with the fix.node-net-server.test.ts21/21 andnode-net-allowHalfOpen.test.js2/2 unchanged.no test proof · iteration 4 · 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