Repository navigation
fix(node, fastify, aws-lambda): resolve sendStandardResponse when the client disconnects - #174
Conversation
…he client disconnects On HTTP/2 a client disconnect errors the response instead of only closing it: a `RST_STREAM` with an error code (`ERR_HTTP2_STREAM_ERROR`), a `GOAWAY` with an error code (`ERR_HTTP2_SESSION_ERROR`) or a TCP reset (`ECONNRESET`). `sendStandardResponse` rejected with those errors, both when the response was already gone before sending and while writing it, so a caller without its own `try/catch` hit an unhandled rejection, while the same disconnect on HTTP/1.1 resolves. Add `isNodeResponseDisconnectError` and resolve on such errors in the node, fastify and aws-lambda adapters. Local failures (body conversion, header application, a response destroyed with any other error) still reject. Closes #163 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STPvYhLSo6ARPkLyz3ecHY
- aws-lambda: run the existing clean-close tests over a client disconnect too, instead of near-copies of them - node: reuse `createEndlessBody` in the http2 HEAD test, and `promiseWithResolvers` in the before-sending disconnect tests - node, fastify: let one promise adopt the send result instead of a separate "handled" deferred Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STPvYhLSo6ARPkLyz3ecHY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STPvYhLSo6ARPkLyz3ecHY
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Disconnect classification: new
isNodeResponseDisconnectErrorinpackages/node/src/utils.tsmatchesERR_HTTP2_STREAM_ERROR,ERR_HTTP2_SESSION_ERROR,ECONNRESET, andEPIPEbyerror.code, and is re-exported through the package index so the fastify and aws-lambda imports resolve. - Adapter behavior: the node, fastify, and aws-lambda
sendStandardResponseimplementations now release the body and resolve when a disconnect is detected before sending, and only reject non-disconnect errors while streaming; the existing'close'listener remains the resolve path. - Docs and tests: READMEs updated for all three adapters, plus unit tests for the helper and HTTP/2 client-disconnect matrices covering cancel, refused/internal-error resets, GOAWAY, and TCP reset, both before and while sending. Targeted run of the four affected test files: 97 tests pass.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Closes #163
Problem
On HTTP/2, Node reports most client disconnects as an error on the stream. On HTTP/1.1 the same disconnect only closes the response.
sendStandardResponserejected with those errors in two places. A server that awaits it without its owntry/catchgets an unhandled rejection and exits.!canWriteToNodeResponse(res)rejected withgetNodeResponseError(res).connection.once('error', reject)caught the same errors, for example in the middle of an event stream.Reproduced on Node 22. Each cell shows the result before → after this PR:
RST_STREAMCANCELRST_STREAMREFUSED_STREAM/INTERNAL_ERRORERR_HTTP2_STREAM_ERROR→ resolvesGOAWAYPROTOCOL_ERRORERR_HTTP2_SESSION_ERROR→ resolvesECONNRESET→ resolvesThe issue doesn't list the TCP reset. On HTTP/1.1 it resolves, so it's the same parity bug, and this PR fixes it too.
Changes
isNodeResponseDisconnectError(error)in@standard-server/node, exported with the other response utils. It returnstrueforERR_HTTP2_STREAM_ERROR,ERR_HTTP2_SESSION_ERROR,ECONNRESETandEPIPE. The tests don't triggerEPIPEfor real; only the helper's unit test covers it.'close'listener resolve, as on HTTP/1.1.ERR_STREAM_WRITE_AFTER_ENDon HTTP/2HEAD, and body stream errors. The existing tests for these are unchanged.getNodeResponseErroris unchanged. It still returns the raw error, which is also passed to the body when it is destroyed, so a source can still see why it was cancelled.Tests
INTERNAL_ERRORtest now expects the send to resolve. Five client actions (cancel, refused-stream reset, internal-error reset,GOAWAY, TCP reset) each run twice, while sending and before sending. In each case the send must resolve and the body must be released.GOAWAY, each while sending and before sending.utils.test.ts: unit tests for the helper.The 14 new disconnect cases fail against the adapters on
mainand pass here; theCANCELcases pass on both, sinceCANCELalready resolved. One test does a PING round-trip before the TCP reset. Without it, Node sometimes reports the reset as a plain close, and the test would pass even without the fix.Locally on Node 22:
pnpm run checkpasses, the full vitest suite passes (1393 tests), and so do the Bun tests. I couldn't run Node 20, 24 or 26 locally, so that relies on the CI matrix.Generated by Claude Code