Skip to content

fix(node, fastify, aws-lambda): resolve sendStandardResponse when the client disconnects - #174

Merged
dinwwwh merged 3 commits into
mainfrom
claude/laughing-mendel-v3zy4o
Oct 26, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/laughing-mendel-v3zy4o

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 26, 2026

Copy link
Copy Markdown
Member

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. sendStandardResponse rejected with those errors in two places. A server that awaits it without its own try/catch gets an unhandled rejection and exits.

  1. The client is gone before the response is sent. !canWriteToNodeResponse(res) rejected with getNodeResponseError(res).
  2. The client leaves while the response is being written. 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:

Client action Before sending While sending
RST_STREAM CANCEL resolves → resolves resolves → resolves
RST_STREAM REFUSED_STREAM / INTERNAL_ERROR rejects ERR_HTTP2_STREAM_ERROR → resolves rejects → resolves
GOAWAY PROTOCOL_ERROR rejects ERR_HTTP2_SESSION_ERROR → resolves rejects → resolves
TCP reset rejects ECONNRESET → resolves rejects → resolves

The 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

  • New isNodeResponseDisconnectError(error) in @standard-server/node, exported with the other response utils. It returns true for ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_SESSION_ERROR, ECONNRESET and EPIPE. The tests don't trigger EPIPE for real; only the helper's unit test covers it.
  • The node, fastify and aws-lambda adapters use it at both points:
    • Before sending: they release the body and resolve.
    • While sending: they ignore the error and let the existing 'close' listener resolve, as on HTTP/1.1.
  • Any other error still rejects, so callers can still send a fallback 500. That covers a response destroyed with a non-disconnect error, header application throwing, body conversion throwing, ERR_STREAM_WRITE_AFTER_END on HTTP/2 HEAD, and body stream errors. The existing tests for these are unchanged.
  • READMEs: the fastify and aws-lambda wording now says a client disconnect resolves, and the node README gets the same sentence.

getNodeResponseError is 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

  • node: the existing INTERNAL_ERROR test 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.
  • fastify: an error reset and a GOAWAY, each while sending and before sending.
  • aws-lambda: the existing clean-close tests now also run with a client disconnect.
  • utils.test.ts: unit tests for the helper.

The 14 new disconnect cases fail against the adapters on main and pass here; the CANCEL cases pass on both, since CANCEL already 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 check passes, 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

…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
@pkg-pr-new

pkg-pr-new Bot commented Oct 26, 2026

Copy link
Copy Markdown
@standard-server/aws-lambda

npm i https://pkg.pr.new/@standard-server/aws-lambda@174

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@174

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@174

@standard-server/fetch

npm i https://pkg.pr.new/@standard-server/fetch@174

@standard-server/node

npm i https://pkg.pr.new/@standard-server/node@174

@standard-server/peer

npm i https://pkg.pr.new/@standard-server/peer@174

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@174

commit: b855e39

@codecov

codecov Bot commented Oct 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

1 warning

⚠️ 3 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/laughing-mendel-v3zy4o (b855e39) with main (e48027d)2

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (f93cacb) during the generation of this report, so e48027d was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Disconnect classification: new isNodeResponseDisconnectError in packages/node/src/utils.ts matches ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_SESSION_ERROR, ECONNRESET, and EPIPE by error.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 sendStandardResponse implementations 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.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit 1d1d4d4 into main Oct 26, 2026
13 checks passed
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.

sendStandardResponse rejects on client resets (HTTP/2 RST_STREAM / GOAWAY) instead of resolving

2 participants