fix(peer): cancel the server request when the client fails after sending it - #144
Conversation
…ing it An exception thrown in transmitRequest after the request message went out (e.g. a request body ReadableStream the caller already locked, which makes the OctetStreamTransmitter constructor throw) was handled with closeById, which sends no cancel. The server kept the request open, its handler blocked on the body, until the peer closed. Once the request message is sent, failures now go through abortById so the server receives a cancel. A failed cancel delivery is swallowed so it cannot surface as an unhandled rejection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Pu97o66FJwoRA8ZmYjFWo
The outer catch in transmitRequest already cancels the server request once the request message is sent, so the per-transmitter .catch blocks are redundant. Their guard (skip the abort when the transmitter was cleared) moves to the outer catch as a streamCancelled check: a stream/cancel from the server is the only way a transmitter is cleared while the request stays open, so a failing transmitter after it is expected and the request keeps waiting for its response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Pu97o66FJwoRA8ZmYjFWo
transmitRequest is fire-and-forget and settles the request itself before anything can fail, so a rejection from it (e.g. a failed cancel delivery) has no observer. Catch it once where it is started instead of inside the outer catch, and drop comments whose behavior the tests already cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Pu97o66FJwoRA8ZmYjFWo
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — error handling for request-body streaming failures after the request message has been transmitted in the packages/peer client.
- Cancel on post-transmission body failure —
ClientPeer.transmitRequestnow branches onstate.requestSent: a failure before the request is sent closes locally (closeById), while a failure after it sends acancelmessage viaabortById, so the server releases the request and aborts its signal. This closes the gap where errors thrown outsidetransmitter.transmit()— e.g.new OctetStreamTransmitter(...)on an already-locked body stream — previously fell through tocloseByIdand leaked the server-side request. - Centralized transmitter error handling — the per-transmitter
.catchblocks were removed; the bareawait transmitter.transmit()now feeds the single outercatch. This is behavior-equivalent for transmit errors (the old.catchguard corresponded to!state.streamCancelled) and covers the constructor-throw path too. - Swallowed call-site rejection —
request()now doesvoid this.transmitRequest(...).catch(() => {})so a failingcancelsend()insideabortByIdcannot surface as an unhandled rejection. - Test coverage — two unit tests for the locked-body path (request rejects with
TypeErrorandsendobservesrequestthencancel; cancel-transport failure stays silent) plus an encoded-wire integration test asserting the server signal aborts and the server request map is emptied.
I verified the new tests are discriminating: reverting client.ts to HEAD~3 while keeping the tests makes all three fail, and the full peer suite (75 tests) passes at head. The guard substitution is equivalent — transmitter fields are only cleared by closeById/abortById (both delete the request state, making abortById a no-op) or by the stream/cancel handler (which sets streamCancelled) — and untransmittedBody is cleared before the transmitter block, so the finally path is unchanged.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Resolve the conflict with #144 in `ClientPeer.transmitRequest`, keeping both behaviors: - From #144: request body failures reach `transmitRequest`'s catch, which cancels the request on the server once the request message is out, unless the server already cancelled the upload. `transmitRequest` is caught at its call site. - From this branch: the body streams alongside the request send, and "out" means handed to `send` (`requestDispatched`), not `send` resolved. The body is now transmitted by an async `transmitBody` helper, so a transmitter that throws on construction (e.g. a locked stream) becomes a rejection awaited with the request send instead of leaving a pending send rejection unhandled. `requestDispatched` is cleared when the request send itself fails, so a failed request send still closes the request without a cancel, as on main. `rejects when send throws` now asserts that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013omMDjkeMkCYsDiw74J54s

Summary
This PR improves error handling when a request body fails to stream after the request message has already been sent to the server. Previously, such failures could leave server-side resources in an inconsistent state. Now the client properly sends a cancel message to the server and handles transport failures gracefully.
Key Changes
.catch()blocks that were attempting to abort on transmission errors; error handling is now centralized in the try-catch blockImplementation Details
state.requestSentflag to determine whether the request message has been transmittedstate.streamCancelledto avoid redundant cancellation attemptshttps://claude.ai/code/session_019Pu97o66FJwoRA8ZmYjFWo