Repository navigation
Stop aborted request bodies from logging duplicate unhandled rejections in @astrojs/node - #17971
Merged
Merged
Conversation
…ne mode When a client disconnects mid-request, Node destroys the incoming message with an ECONNRESET error that propagates through the request body stream. With a custom src/fetch.ts handler, request.json() rejects and the error escapes app.render() into the standalone handler's floating promise, surfacing as an unhandled rejection. The same rejection was logged twice because createAppHandler registered a process-wide unhandledRejection listener on every call (the standalone entrypoint calls it once for the exported handler and again when starting the server). The unhandledRejection listener is now registered once at module level and tracks the request URL through an AsyncLocalStorage context. Errors caused by a client disconnect are tagged on the exact error object (WeakSet) and dropped silently; genuine rejections are still logged once with the request URL.
…ntries Revision addressing adversarial review findings: - A rejected src/fetch.ts handler is now handled at the adapter boundary where the request URL and response are still available: it is logged once via the adapter logger with the URL and completes with a 500 (or destroys the connection once headers were sent), instead of floating as a process unhandled rejection that could not be attributed and left the client waiting. The process-level unhandledRejection listener is now reserved for genuinely detached promises. - The unhandledRejection listener, request context, and aborted-error registry are shared process-wide through globalThis so that multiple built standalone entries loaded into one process register a single listener and route rejections to the app that handled the request, instead of logging each rejection once per entry. - Tests: the abort test now synchronizes on the fixture signal that body reading started and asserts output stability over an interval; a new test covers a fetch handler that rejects directly (one URL-bearing log + 500); a new suite starts two built entries in one child process and verifies aborts stay silent and each unhandled rejection is logged exactly once.
The duplicate log from a faulty multi-listener implementation could arrive in a later event-loop turn than the first line, so the two-entry assertions now wait for the child output to stop growing before slicing, matching the abort tests.
🦋 Changeset detectedLatest commit: e3c2eea The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
e18e dependency analysisNo dependency warnings found. |
matthewp
marked this pull request as ready for review
September 14, 2026 23:17
Merged
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
When a client disconnected while a custom
src/fetch.tshandler was readingrequest.json(), Node rejected the body read withECONNRESET. The standalone Node adapter let that error escape, so it was logged as an unhandled rejection—often twice.createAppHandler()now catches render failures:Each app handler owns its request context and aborted-error tracking. A handler only reports detached rejections created in its own request context, so other handlers do not log the same rejection.
This prevents aborted body reads from reaching
unhandledRejection, without hiding unrelatedECONNRESETerrors from user code.Fixes #17966
Testing
src/fetch.tsstarts reading the body, disconnects, then verifies noECONNRESETor unhandled-rejection output is logged and the server remains healthy.Docs
No docs update needed because this fixes error handling without changing a public API or configuration.