Skip to content

fix(peer): reject resolving a streamed body more than once - #146

Merged
dinwwwh merged 4 commits into
mainfrom
claude/brave-tesla-hllbnq
Oct 8, 2026
Merged

dinwwwh merged 4 commits into
mainfrom
claude/brave-tesla-hllbnq

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Calling resolveBody() twice on a streamed peer body (event stream or octet stream) now throws TypeError: Failed to read body: body stream already read, like the fetch and node adapters. Before, each call created a new reader of the same message queue. The two readers split the messages between them (one got 0 and 2, the other 1 and 3), and on ServerPeer the second reader could hang forever, because the queue was dropped without being closed.

Fixes

  • A second resolveBody() on an event-stream or octet-stream body throws a TypeError, in both ClientPeer (response bodies) and ServerPeer (request bodies). The first reader still gets every message.
  • Bodies that are not streamed (JSON, files, form data, URL-encoded) can still be resolved more than once, since they are already in memory.
  • ServerPeer now aborts the request body queue when the body finishes, instead of only dropping it, so nothing can be left waiting on it. Before, it only aborted the queue when the body was cancelled.

Testing

  • New client and server tests for both stream types check that a second resolveBody() throws, both before and after the stream ends, and that the first reader still receives every message. They fail against main.
  • New server tests check that the request body queue is aborted once the body is fully read. They hang against main.
  • pnpm run check and pnpm test pass (1336 vitest tests, plus the Bun and Deno suites).

🤖 Generated with Claude Code

https://claude.ai/code/session_012g4ovPb64wru9czTYsrwGe

claude added 4 commits October 7, 2026 03:07
A streamed peer body (event or octet stream) has a single message queue,
but every `resolveBody()` call built a new consumer of it. Two consumers
split the messages between them, and on the server the second one could
hang forever: the success cleanup dropped the queue without closing it,
so its pending pull never settled.

- Throw `TypeError('Failed to read body: body stream already read')` on a
  second `resolveBody()` for streamed bodies, matching the fetch and node
  adapters.
- Close the request body queues in the server's success/error cleanup
  instead of only unsetting them, so nothing is left waiting on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g4ovPb64wru9czTYsrwGe
Once the request body is finished, its single consumer is done, so closing
and aborting the queue behave the same. Abort unconditionally instead of
branching on the cleanup kind; this also releases late buffered messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g4ovPb64wru9czTYsrwGe
The server cleanup now aborts the request body queues, and close and abort
are distinct Queue operations, so the test comments should match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g4ovPb64wru9czTYsrwGe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g4ovPb64wru9czTYsrwGe
@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026

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

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

@standard-server/core

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

@standard-server/fastify

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

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

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

commit: 1b59e60

@codspeed

codspeed Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/brave-tesla-hllbnq (1b59e60) with main (d5d0eb6)

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 8, 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 — full PR (4 files, 4 commits), verified by running the peer suite plus tests/data-transfer.test.ts / tests/signal-and-cancel.test.ts (823 tests passing) and the repo check (sherif/oxlint/oxfmt/tsc).

  • Single-read guard for streamed peer bodies — toStandardBody now wraps the event/octet stream resolvers in resolveStreamBodyOnce, so a second resolveBody() throws the same TypeError as the fetch adapter instead of building a second consumer that splits the queue.
  • Unconditional request-body queue abort — the server cleanup callback now aborts both queues (was only on cancelled && streamActive) before unsetting them, so a pending pull() on a dropped queue always settles. stream/cancel is still emitted only on cancelled + active.
  • Tests — new server/client cases for event- and octet-stream double-resolve, and assertions that the dropped queue rejects with Queue was aborted..

The guard's closure flag and the abort-on-success path are both safe: success/error cleanup only runs after the terminator is consumed, when no legitimate buffered message or pending pull remains, and the atomic/json branches (which don't share a queue) are correctly left unguarded.

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

@dinwwwh dinwwwh changed the title Prevent multiple resolveBody calls on streamed request/response bodies fix(peer): reject resolving a streamed body more than once Oct 8, 2026
@dinwwwh
dinwwwh merged commit a03d8bf into main Oct 8, 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.

2 participants