Skip to content

fix(node): stop crashing when a web stream's source errors before its first read - #149

Merged
dinwwwh merged 3 commits into
mainfrom
claude/epic-mccarthy-gncjff
Oct 8, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/epic-mccarthy-gncjff

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 8, 2026

Copy link
Copy Markdown
Member

toWebReadableStream pulls through the source's async iterator, which only attaches its error listener on its first next(), during the web stream's first pull. Until then an error from the source had no listener, so it became an uncaught exception and crashed the process. Two ways to hit it:

  • Cancelling with an Error before any read, e.g. toWebReadableStream(source).cancel(new Error('boom')). Cancel destroys a non-request source with the reason, and destroy(err) emits error on the next tick.
  • A source that errors in the same tick it is wrapped. The web stream's first pull is a microtask, so from a timer or I/O callback the source's nextTick error arrives first.

Fixes

  • A no-op error listener is attached next to the iterator, the way destroyNodeHttpBody attaches one before destroying. The first read still rejects with the source's error, and cancel still leaves the source errored with the cancel reason.
  • Server requests were never at risk from cancel, since they are drained instead of destroyed. The only change for them: if the client aborts before the handler's first read, that read now rejects with aborted instead of Premature close, because IncomingMessage only records an abort error when it has an error listener.

Testing

  • New toWebReadableStream tests cancel a fresh Readable with an Error before any read, and error a source in the same tick it is wrapped. Both fail against main with an uncaught exception.
  • A new test covers a request torn down while its cancelled body drains. That path's catch was previously only reached by timing luck in the aborted-upload tests, so function coverage of utils.ts flickered below 100%.
  • pnpm run check and pnpm run test:coverage pass (1349 vitest tests, plus the Bun and Deno suites), and packages/node/src/utils.ts is at 100% coverage.

🤖 Generated with Claude Code

https://claude.ai/code/session_011tc83hdkAdsVixQ5ywKLMD


Generated by Claude Code

claude added 3 commits October 8, 2026 02:11
… 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
@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026

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

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

@standard-server/core

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

@standard-server/fastify

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

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

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

commit: bcad86f

@codecov

codecov Bot commented Oct 8, 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 8, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/epic-mccarthy-gncjff (bcad86f) with main (668cc70)

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. ↩

@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 critical issues — one minor suggestion inline.

Reviewed changes

  • Early source errors are now caught: toWebReadableStream attaches a no-op error listener at wrap time, so an error emitted before the iterator's first next() (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 aborted instead of Premature 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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread packages/node/src/utils.ts
@dinwwwh
dinwwwh merged commit 6dfa912 into main Oct 8, 2026
11 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.

2 participants