fix(fetch): cancel the body when toFetchResponse throws - #128
Conversation
`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>
@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 new issues found.
Reviewed changes — reviewed the complete diff of 52bd2e2 (initial review).
packages/fetch/src/response.ts: wrapsnew Response(...)andvoid response.bodyin atry; on throw, cancels aReadableStreambody viabody.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 (assertscancelgets theTypeError), 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.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

toFetchResponsenow cancels the response body when building theResponsethrows. Before, an event-stream body was already pulling from its iterator whentoFetchHeadersornew Responsefailed, so the iterator was never released (a generator'sfinallynever ran) and the 15s keep-alive interval kept firing for the life of the process.Fixes
Testing
main.vitest run(1306 passed), pluseslintandtsc -bforpackages/fetch, are clean.🤖 Generated with Claude Code