Repository navigation
fix(peer): send the cancel after an in-flight request message - #119
Conversation
When a client request was aborted while its request message was still being sent (e.g. `encodePeerMessage` awaiting `blob.arrayBuffer()`), the cancel message could reach the server first. The server ignores cancels for ids it has not seen, so the handler then ran un-aborted and a streaming response kept going until the peer closed. The client now sends the cancel again once the in-flight request message is sent. The immediate cancel is kept for transports whose `send` only resolves after the server handled the message, where it is the only way to abort the running handler. A duplicate cancel is a no-op on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSY5GvoAft9apocjwoSQrk
- Track a single `cancelSent` flag instead of two: `transmitRequest` only finds the state gone right after the request send resolves, so the in-flight window is already implied there. - Fire the resent cancel without awaiting it, so releasing the untransmitted request body is not delayed by another `send`. - Tests: give `connect()` `awaitRemote` and `beforeClientSend` options instead of copying the wire setup, run the client abort propagation test for both transport styles, share the abort-while-sending setup in the client tests, and fold a duplicate test into the existing one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSY5GvoAft9apocjwoSQrk
Instead of cancelling right away and resending afterwards, track whether the request message has been sent. `abortById` only sends the cancel once it has; otherwise `transmitRequest` checks the signal after the request send and awaits the cancel there. This sends a single cancel, always after the request, and none at all when the request is aborted before it is sent. Transports whose `send` only resolves after the server has handled the request no longer get a cancel while that handling runs, so the abort test for that transport style is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSY5GvoAft9apocjwoSQrk
- Explain why the cancel is held back once, on `requestSent`, instead of at both call sites. - Client tests: drive the abort-while-sending helper from the `send` mock and the released body instead of polling, and fold the failed-cancel test into the existing abort-during-send body test. - Integration tests: restore `connect()` and its wiring from main, and hold back the real `file.arrayBuffer()` read instead of a custom hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSY5GvoAft9apocjwoSQrk
… catch
Drop the `.catch(() => {})` on the cancel sent after the request message.
A failed delivery is already handled by the surrounding try/catch, which
closes the request and cancels the unsent body with that error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSY5GvoAft9apocjwoSQrk
@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 — verified the client-side cancel ordering fix end to end and traced the abort racing paths; the full suite (1244 tests), eslint, and tsc -b pass.
- Defer cancel until the request message is sent —
ClientPeerRequestStateInternal.requestSentgatesabortById'scancelsend (client.ts:279), withtransmitRequestemitting the held-back cancel once the in-flight request send resolves (client.ts:99-105). - No cancel when the request was never sent — abort during atomic-body encode now sends nothing (previously sent a cancel that the server dropped), and a rejected request
sendintentionally sends no cancel. - Tests — new unit helper/tests for abort during an in-flight
send(cancel after delivery / none on send failure), plus an integration test aborting mid-encode that only passes with the corrected ordering; existing expectations updated accordingly.
The reasoning is sound: the early set of requestSent and the post-send check share one synchronous block, so no double-cancel window exists, and the server's closeById already no-ops for unknown ids, which is exactly why the old overtaking cancel was lost. I traced the stream-transmit abort path and the peer-close path as unaffected.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Summary
When a
ClientPeerrequest was aborted while its request message was still being sent, thecancelmessage could reach the server first. For example, the README transportport.postMessage(await encodePeerMessage(m))awaitsblob.arrayBuffer()forFile/FormDatabodies.ServerPeerignores a cancel for an id it has not seen, so the handler then ran withsignal.aborted === false, and a streaming response kept going until the peer closed.Changes
ClientPeertracksrequestSent, which is set oncesend(request)resolves.abortByIdsends the cancel only if the request message has been sent. OtherwisetransmitRequestchecksrequest.signal?.abortedafter the request send and awaits the cancel there, so it always follows the request.Trade-off
On transports whose
sendonly resolves after the server has handled the request, the cancel is no longer sent while that handling runs.Tests
file.arrayBuffer()is being read during encoding still aborts the server handler.cancelmessage to expecting no messages, and one now waits for the cancel instead of asserting it immediately.🤖 Generated with Claude Code
https://claude.ai/code/session_01TSY5GvoAft9apocjwoSQrk