Skip to content

fix(fetch): cancel the body when toFetchResponse throws - #128

Merged
dinwwwh merged 1 commit into
mainfrom
claude/tofetchresponse-stream-cleanup-3fbd11
Sep 29, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/tofetchresponse-stream-cleanup-3fbd11

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

toFetchResponse now cancels the response body when building the Response throws. Before, an event-stream body was already pulling from its iterator when toFetchHeaders or new Response failed, so the iterator was never released (a generator's finally never ran) and the 15s keep-alive interval kept firing for the life of the process.

Fixes

  • Bodies are released when the response can't be built: a 204/205/304 status with a body, a status outside 200–599, or an invalid header name/value.
  • The original error is still thrown unchanged. A stream that refuses to cancel (locked) no longer surfaces as an unhandled rejection.
  • The success path has no extra cost.

Testing

  • New tests: an event-stream body with a 204 status, a stream body with an invalid header, and a locked stream. The first two fail against main.
  • Full vitest run (1306 passed), plus eslint and tsc -b for packages/fetch, are clean.

🤖 Generated with Claude Code

`toFetchBody` returns an event stream that is already pulling from its
iterator with the keep-alive interval armed. If `toFetchHeaders` or the
`Response` constructor then threw (a 204/205/304 status with a body, a
status outside 200-599, an invalid header), the stream was never
cancelled: the iterator was never released and the interval kept firing.

Building the response now runs in a `try`, and on failure a stream body
is cancelled with the error before rethrowing it. A rejected cancel (a
locked stream) is ignored so it cannot hide the original error.

Co-Authored-By: Claude <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026

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

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

@standard-server/core

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

@standard-server/fastify

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

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

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

commit: 52bd2e2

@codecov

codecov Bot commented Sep 29, 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 Sep 29, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/tofetchresponse-stream-cleanup-3fbd11 (52bd2e2) with main (28db1dd)

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 new issues found.

Reviewed changes — reviewed the complete diff of 52bd2e2 (initial review).

  • packages/fetch/src/response.ts: wraps new Response(...) and void response.body in a try; on throw, cancels a ReadableStream body via body.cancel(error).catch(() => {}) and rethrows the original error. Success path is byte-for-byte equivalent.
  • packages/fetch/src/response.test.ts: three new cases — event-stream body with a 204 status (asserts iterator cleanup runs), stream body with an invalid header value (asserts cancel gets the TypeError), and a locked stream (asserts the original /locked/ error still propagates without an unhandled rejection).

I independently confirmed the tests are discriminating: restoring response.ts to base 28db1dd fails the two cleanup cases, and dropping .catch(() => {}) makes the locked-stream case surface an unhandled ERR_INVALID_STATE. Full vitest run (1306 passed), tsc -b for packages/fetch, and eslint on both changed files are clean. The event stream is the only body source that starts pulling before new Response, and toFetchBody cannot throw after toEventStream returns, so the leak is fully covered by the new try.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit c32b7be into main Sep 29, 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.

1 participant