Skip to content

fix(peer): cancel the server request when the client fails after sending it - #144

Merged
dinwwwh merged 3 commits into
mainfrom
claude/adoring-faraday-mdh2we
Oct 4, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/adoring-faraday-mdh2we

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 4, 2026

Copy link
Copy Markdown
Member

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

  • Request body streaming error handling: When body transmission fails after the request message is sent, the client now sends a cancel message to the server instead of just closing the request locally
  • Conditional abort vs close logic: Distinguishes between two failure scenarios:
    • If the request hasn't been sent yet: close the request locally
    • If the request was sent but body streaming failed: send a cancel message to notify the server
  • Transport failure resilience: Silently ignores transport errors when sending the cancel message, preventing cascading failures
  • Removed catch handlers from transmitters: Simplified transmitter error handling by removing the .catch() blocks that were attempting to abort on transmission errors; error handling is now centralized in the try-catch block
  • Test coverage: Added comprehensive tests for:
    • Sending cancel messages when body cannot be read after request transmission
    • Silently handling transport failures during cancellation
    • Integration test verifying server-side request cleanup when client body streaming fails

Implementation Details

  • The fix leverages existing state.requestSent flag to determine whether the request message has been transmitted
  • Uses state.streamCancelled to avoid redundant cancellation attempts
  • The cancel message ensures the server can properly clean up resources and abort the request signal on its end

https://claude.ai/code/session_019Pu97o66FJwoRA8ZmYjFWo

claude added 3 commits October 3, 2026 12:16
…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
@pkg-pr-new

pkg-pr-new Bot commented Oct 4, 2026

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

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

@standard-server/core

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

@standard-server/fastify

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

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

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

commit: 7151006

@dinwwwh dinwwwh changed the title Handle body streaming failures after request transmission fix(peer): cancel the server request when the client fails after sending it Oct 4, 2026
@codspeed

codspeed Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/adoring-faraday-mdh2we (7151006) with main (74059f5)

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. ↩

@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@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 — 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.transmitRequest now branches on state.requestSent: a failure before the request is sent closes locally (closeById), while a failure after it sends a cancel message via abortById, so the server releases the request and aborts its signal. This closes the gap where errors thrown outside transmitter.transmit() — e.g. new OctetStreamTransmitter(...) on an already-locked body stream — previously fell through to closeById and leaked the server-side request.
  • Centralized transmitter error handling — the per-transmitter .catch blocks were removed; the bare await transmitter.transmit() now feeds the single outer catch. This is behavior-equivalent for transmit errors (the old .catch guard corresponded to !state.streamCancelled) and covers the constructor-throw path too.
  • Swallowed call-site rejection — request() now does void this.transmitRequest(...).catch(() => {}) so a failing cancel send() inside abortById cannot surface as an unhandled rejection.
  • Test coverage — two unit tests for the locked-body path (request rejects with TypeError and send observes request then cancel; 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.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit 2f06dd6 into main Oct 4, 2026
11 checks passed
dinwwwh pushed a commit that referenced this pull request Oct 4, 2026
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
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.

2 participants