Skip to content

fix(peer, core): cancel stream bodies when a request ends before they are sent - #113

Merged
dinwwwh merged 3 commits into
mainfrom
claude/peer-body-cleanup-f6c58c
Sep 26, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/peer-body-cleanup-f6c58c

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 26, 2026

Copy link
Copy Markdown
Member

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

… 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`
@pkg-pr-new

pkg-pr-new Bot commented Sep 26, 2026 •

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

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

@standard-server/core

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

@standard-server/fastify

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

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

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

commit: 955be62

@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/peer-body-cleanup-f6c58c (955be62) with main (e5eb0aa)

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

@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 critical issues — one minor consistency nit inline.

Reviewed changes

  • New cancelStandardBody(body, reason?) in @standard-server/core — cancels a ReadableStream or calls return() on an async iterator, no-ops for other bodies, and rejects if the release rejects.
  • ServerPeer.message body cleanup — captures response.body after the handler resolves, clears it before any transmitter is constructed, and cancels it in a finally on the abort/close/send-failure paths. Cleanup failures reject message() (documented, "like a handler error").
  • ClientPeer.transmitRequest body cleanup — same capture/clear/finally shape, but cleanup failures are swallowed since the request has already settled; the old inline transmitter.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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/peer/src/client.ts Outdated
…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

@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

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's finally now passes failure ?? 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 send asserts cancel receives the exact serialized abort reason; I confirmed it fails against 7340fc1 (reason arrives as undefined).
  • Added a regression test — does not send cancel message when transport fails after server already canceled the upload guards against a spurious cancel message on the transport-failure-after-server-cancel path.

The prior review's client.ts:125 thread is addressed and resolved.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit a545524 into main Sep 26, 2026
11 checks passed
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.

1 participant