Skip to content

sockets: close() with a pipe close in flight logged an uncaught exception - #7313

Merged
guybedford merged 3 commits into
mainfrom
gbedford/socket-close-pending-pipe
Sep 11, 2026
Merged

sockets: close() with a pipe close in flight logged an uncaught exception#7313
guybedford merged 3 commits into
mainfrom
gbedford/socket-close-pending-pipe

Conversation

@guybedford

@guybedford guybedford commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

A connect() handler that pipes an inbound socket to another and then closes both concurrently logged Uncaught (in promise): undefined and an internal-error line (jsg.Error: undefined) on every connection, or Network connection lost under a peer disconnect, with nothing actually unhandled and the handler completing normally.

Two causes:

  • Socket::close() aborted its streams with no reason, which WritableStreamInternalController::abort() turns into undefined (as the spec requires for the user-facing call), so everything that abort rejected rejected with a bare undefined. It now aborts with a real TypeError.
  • When a pipe into the socket had just completed, its close of the writable was still queued; the forced abort drained it, and the write loop's pipe-completion path returned that close's promise, so the rejection failed the loop's awaitJs task and was reported as uncaught. The close's outcome belongs to the close request alone, so the loop no longer propagates it.

A genuine throw from a connect() handler is still reported as uncaught.

Tests: closeWithPipeCloseInFlight in the streams sockets suite runs the scenario under both stream implementations and asserts the observable half, that the abort reason a later writer sees is a real TypeError and never undefined; it fails on main under C++ at the current compat date, and passes where the race cannot occur (TypeScript resolves pipeTo only after the destination close completes; before internal_writable_stream_abort_clears_queue the abort defers until the queued close finishes). The logged line itself is awaitJs's INTERNAL_ASYNC report of a rejected promise handed to C++, not a V8 unhandled rejection (the promise did have a handler), so unhandledrejection never fires and nothing else is JS-observable; server/tests/socket-close spawns workerd test and asserts the log is clean, and fails on main with the exact reported lines. Streams, sockets, connect and net suites pass.

…tion

Socket::close() aborted its streams with no reason, which abort() turns
into undefined, so anything rejected by that abort rejected with a bare
undefined. When a pipe into the socket had just completed, its close of
the writable was still queued; the forced abort drained it, and the write
loop's pipe-completion path returned that close's promise, so the
rejection failed the loop's task and surfaced as
"Uncaught (in promise) undefined" plus an internal-error log, though
nothing was actually unhandled. Under a peer disconnect the same path
reported "Network connection lost".

close() now aborts with a real TypeError, and the pipe-completion path
no longer couples the write-loop task to the close's outcome, which
belongs to the close request alone. A genuine handler throw is still
reported. The test spawns workerd and asserts on its log output, since
the test framework does not observe these.
@guybedford
guybedford requested review from a team as code owners September 11, 2026 04:09
Comment thread src/workerd/server/tests/socket-close/socket-close-pending-pipe.js
Comment thread src/workerd/api/streams/internal.c++ Outdated
Comment thread src/workerd/api/streams/internal.c++ Outdated
Comment thread src/workerd/api/sockets.c++ Outdated
@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

LGTM

github run

Comment thread src/workerd/server/tests/socket-close/socket-close-pending-pipe.js Outdated
Comment thread src/workerd/server/tests/socket-close/socket-close-pending-pipe.js Outdated
Comment thread src/workerd/server/tests/socket-close/socket-close-pending-pipe.js Outdated
Comment thread src/workerd/server/tests/socket-close/socket-close-pending-pipe.js Outdated
The pipe-completion paths no longer return the close's promise at all:
the close is its own request with its own write loop, so nothing waits
on it. Drop the redundant JsValue wrap. The scenario also runs in the
streams sockets suite under both implementations, asserting the abort
reason a later writer observes is a real error and never undefined; the
spawn-based test remains the only observer of the uncaught-exception log.
The closed promises are awaited rather than swallowed, pipe and close
results are asserted, and the echo reads with for await. The client
half-closes and drains without calling close(): at the oldest compat
date a client close() after EOF leaves the server side's close()
pending, which is unrelated to this change.
@guybedford
guybedford merged commit 7d23293 into main Sep 11, 2026
34 checks passed
@guybedford
guybedford deleted the gbedford/socket-close-pending-pipe branch September 11, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants