Repository navigation
fix(node): stop crashing when a web stream's source errors before its first read - #149
Conversation
… before its first read `toWebReadableStream`'s cancel destroys non-request sources with the cancel reason, and `destroy(err)` emits `error` on the next tick. The source's async iterator only attaches its error listener once the first `pull` runs, so cancelling with an `Error` before any read left the event unhandled and crashed the process. Attach a no-op `error` listener before destroying, as `destroyNodeHttpBody` already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tc83hdkAdsVixQ5ywKLMD
The drain's `catch` was only reached by chance timing in the aborted-upload tests, so function coverage of `utils.ts` flickered below 100%. Destroy a server request mid-drain to hit it deterministically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tc83hdkAdsVixQ5ywKLMD
… created The async iterator only attaches its `error` listener on its first `next()`, so the gap isn't specific to cancel: a source that errors in the same tick it is wrapped also crashed the process. Attach the no-op listener next to the iterator instead of in the cancel branch; the first read still rejects with the source's error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tc83hdkAdsVixQ5ywKLMD
@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 performance
Comparing Footnotes
|
There was a problem hiding this comment.
ℹ️ No critical issues — one minor suggestion inline.
Reviewed changes
- Early source errors are now caught:
toWebReadableStreamattaches a no-operrorlistener at wrap time, so anerroremitted before the iterator's firstnext()(cancel-with-Error before any read, or a same-tick source error) no longer becomes an uncaught exception. - Server requests unchanged in intent: they are still drained rather than destroyed on cancel; the only observable shift is an abort-before-first-read now rejecting with
abortedinstead ofPremature close. - Tests: two new cases exercise cancel-before-read and same-tick error; a third covers a request torn down while its cancelled body drains, restoring 100% function coverage of
utils.ts.
Verified directly that an error emitted before the first read and between two pulls both reject the pending read (no silent done), and CI is green on Node 20/22/24/26.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

toWebReadableStreampulls through the source's async iterator, which only attaches itserrorlistener on its firstnext(), during the web stream's firstpull. Until then anerrorfrom the source had no listener, so it became an uncaught exception and crashed the process. Two ways to hit it:Errorbefore any read, e.g.toWebReadableStream(source).cancel(new Error('boom')). Cancel destroys a non-request source with the reason, anddestroy(err)emitserroron the next tick.pullis a microtask, so from a timer or I/O callback the source'snextTickerrorarrives first.Fixes
errorlistener is attached next to the iterator, the waydestroyNodeHttpBodyattaches one before destroying. The first read still rejects with the source's error, and cancel still leaves the sourceerroredwith the cancel reason.abortedinstead ofPremature close, becauseIncomingMessageonly records an abort error when it has anerrorlistener.Testing
toWebReadableStreamtests cancel a freshReadablewith anErrorbefore any read, and error a source in the same tick it is wrapped. Both fail againstmainwith an uncaught exception.catchwas previously only reached by timing luck in the aborted-upload tests, so function coverage ofutils.tsflickered below 100%.pnpm run checkandpnpm run test:coveragepass (1349 vitest tests, plus the Bun and Deno suites), andpackages/node/src/utils.tsis at 100% coverage.🤖 Generated with Claude Code
https://claude.ai/code/session_011tc83hdkAdsVixQ5ywKLMD
Generated by Claude Code