fix(peer, core): cancel stream bodies when a request ends before they are sent - #113
Conversation
… are sent When a request ended before its stream body reached a transmitter, the peer never cancelled that body. A `ReadableStream` or async iterator returned by a server handler, or passed as a client request body, stayed open and its cleanup never ran. Both peers now release such a body when a cancel, `close()`, an abort, or a failed send ends the request first. ## Fixes - Server: a stream response body is cancelled when the client cancels or the peer closes while the handler runs, while the response is encoded or sent, or when sending the response fails - Client: a stream request body is cancelled when the request is aborted or the peer is closed during encoding, or when sending the request fails ## Behavior - If a server handler's stream body fails to clean up, `ServerPeer.message()` rejects with that error, like a handler error - Client-side cleanup failures are ignored, since the request has already settled ## New API - `cancelStandardBody(body, reason?)` in `@standard-server/core` cancels a `ReadableStream` or returns an async iterator that will not be consumed, and rejects if that cleanup fails ## Testing - New peer server and client tests for each path; they failed before the fix - Unit tests for `cancelStandardBody`
@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 consistency nit inline.
Reviewed changes
- New
cancelStandardBody(body, reason?)in@standard-server/core— cancels aReadableStreamor callsreturn()on an async iterator, no-ops for other bodies, and rejects if the release rejects. ServerPeer.messagebody cleanup — capturesresponse.bodyafter the handler resolves, clears it before any transmitter is constructed, and cancels it in afinallyon the abort/close/send-failure paths. Cleanup failures rejectmessage()(documented, "like a handler error").ClientPeer.transmitRequestbody cleanup — same capture/clear/finallyshape, but cleanup failures are swallowed since the request has already settled; the old inlinetransmitter.cancel()branches collapse into a single early return.- Tests — core unit tests for
cancelStandardBody, plus peer server/client cases for each path.
I verified the new peer tests are discriminating: restoring packages/peer/src/server.ts and client.ts to the parent commit fails 9 of them. I also traced every return/catch/finally in both lifecycle methods (and had it independently checked): untransmittedBody is cleared synchronously before any transmitter is built, so the new finally and the transmitter/queue cancel paths never own the same body — no reachable double-cancel, no cancelling a still-transmitting body, and no missed release.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
…er an abort during send A request aborted while its request message was being sent cancelled its body without a reason; it now receives the signal's abort reason, like every other abort path. ## Testing - New client test for an abort during send; it failed before the fix - New event-stream test for a transport failure after the server stops consuming the upload, covering the last partial branch in client.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run re-reviewed the one commit added since the prior pullfrog review (47259e7), which addresses the earlier client.ts reason-propagation nit.
- Propagated the abort reason to body cleanup —
ClientPeer.transmitRequest'sfinallynow passesfailure ?? request.signal?.reason, so the two early-return paths cancel an untransmitted body with the same reason as the abort-during-encode path. - Added a discriminating test —
cancels the request body with the abort reason when signal aborted during sendassertscancelreceives the exact serialized abort reason; I confirmed it fails against7340fc1(reason arrives asundefined). - Added a regression test —
does not send cancel message when transport fails after server already canceled the uploadguards against a spuriouscancelmessage on the transport-failure-after-server-cancel path.
The prior review's client.ts:125 thread is addressed and resolved.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

When a request ended before its stream body reached a transmitter, the peer never cancelled that body. A
ReadableStreamor async iterator returned by a server handler, or passed as a client request body, stayed open and its cleanup never ran. Both peers now release such a body when a cancel,close(), an abort, or a failed send ends the request first.Fixes
Behavior
ServerPeer.message()rejects with that error, like a handler errorNew API
cancelStandardBody(body, reason?)in@standard-server/corecancels aReadableStreamor returns an async iterator that will not be consumed, and rejects if that cleanup failsTesting
cancelStandardBody