Conversation
…remote FIN Reproduces the bug where calling socket.close() after draining a readable stream to EOF (remote FIN) causes socket.closed to reject with undefined instead of resolving. Two code paths race: Socket::close() aborts the writable with kj::none (→ undefined), then the maybeCloseWriteSide microtask finds the writable errored and rejects closedResolver with the raw undefined value. Three test cases: 1. Core race: close() after for-await EOF → closed rejects undefined 2. Immediate close() → rejection reason must not be undefined 3. No explicit close() → clean remote FIN resolves closed
|
/bonk Only closedRejectsUndefinedAfterCloseRace test fails and only in the all-compat-flags configuration, that seems to mean that there is some specific compat flag causing this bug, can you try to understand which compat flag that is? Then add that flag's "compatDisableFlag" to the test configuration so that we see if disabling the flag indeed makes the test pass. Do it in a new commit |
There was a problem hiding this comment.
Review findings
- [HIGH] Test
closedRejectsUndefinedAfterCloseRacefails at@all-compat-flagson all platforms. This is an intentional regression test for #6479, but since the bug is unfixed, this test breaks CI. See inline comments for details. - [MEDIUM] The
connecthandler has an unhandled promise rejection fromwriter.close()when the client disconnects. Visible in CI logs asUncaught (in promise): Error: Network connection lost. - [LOW] Copyright header year says 2017-2022; new files added in 2026 should use the current year range.
This review was generated by an AI assistant and may contain inaccuracies.
| 'socket.closed rejected with undefined — should resolve or reject with a real Error' | ||
| ); | ||
| } | ||
| }, |
There was a problem hiding this comment.
[HIGH] This test correctly reproduces the bug from #6479, but since the bug is not yet fixed, the test fails at @all-compat-flags across all CI platforms, breaking the build.
The @all-compat-flags variant (compat date 2999-12-31) enables internal_writable_stream_abort_clears_queue (field @57, enable date 2024-09-02). With this flag, Socket::close() → writable->getController().abort(js, kj::none) immediately calls drain() → doError(js, undefined), putting the writable into Errored(undefined) state synchronously. Then when the maybeCloseWriteSide microtask fires, isClosedOrClosing() returns false (it doesn't check for Errored state), so it calls close() on the errored stream, which rejects with undefined, and the catch handler rejects closedResolver with undefined.
Without the flag (at the default compat date 2000-01-01), the lazy abort path handles things differently and the race doesn't manifest — that's why @ and @all-autogates pass.
Per danlapid's comment, the specific flag to investigate is internal_writable_stream_abort_clears_queue. Adding "internal_writable_stream_abort_does_not_clear_queue" to the test's compatibilityFlags would confirm this and make @all-compat-flags pass — though the real fix belongs in Socket::close() or maybeCloseWriteSide.
| async connect(socket) { | ||
| const writer = socket.writable.getWriter(); | ||
| await writer.write(new TextEncoder().encode('ping')); | ||
| await writer.close(); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
[MEDIUM] The connect handler has an unhandled promise rejection. When the client-side test calls socket.close() and tears down the connection, the server-side writer.close() on line 28 rejects with Error: Network connection lost. This shows up in all CI logs as:
workerd/io/worker.c++:2452: info: uncaught exception; source = Uncaught (in promise); stack = Error: Network connection lost.
at async Object.connect (worker:28:5)
Wrapping the body in a try/catch would silence this:
| async connect(socket) { | |
| const writer = socket.writable.getWriter(); | |
| await writer.write(new TextEncoder().encode('ping')); | |
| await writer.close(); | |
| }, | |
| }; | |
| async connect(socket) { | |
| const writer = socket.writable.getWriter(); | |
| try { | |
| await writer.write(new TextEncoder().encode('ping')); | |
| await writer.close(); | |
| } catch { | |
| // Client may disconnect before close completes — expected in these tests. | |
| } | |
| }, |
|
Review posted on PR #6483 with 3 inline comments and a summary. Here's what I found: Key finding: The The 3 review comments cover:
|
The commit is clean. Here's a summary of what was done:
Changes
Three files added/modified to reproduce the bug from the issue:
src/workerd/api/tests/socket-closed-test.js(new)Contains a
connect()handler that writes"ping"then closes (sending FIN), plus three test cases:closedRejectsUndefinedAfterCloseRace— Core bug reproduction. Drains readable viafor await, then callssocket.close(). This races withmaybeCloseWriteSide(queued as a microtask byhandleReadableEof). Theclose()call aborts the writable withkj::none→undefined, so when the microtask fires, the writable is already errored andclosedResolver.rejectreceivesundefined.closedRejectionReasonIsNeverUndefined— Callssocket.close()immediately without draining. Ifsocket.closedrejects, the reason must not beundefined.closedResolvesAfterRemoteEofWithoutExplicitClose— Drains to EOF without callingsocket.close(). Verifies the baseline: a clean remote FIN should resolvesocket.closed.src/workerd/api/tests/socket-closed-test.wd-test(new)Cap'n Proto config defining the test worker with
nodejs_compat_v2+experimentalflags, aninternetnetwork service, and a TCP socket on port 8084.src/workerd/api/tests/BUILD.bazel(modified)Added
wd_testentry forsocket-closed-testwith--experimentalflag andexclusivetag (due to fixed TCP port usage).Closes #6479
github run