Repository navigation
fix(peer): reject resolving a streamed body more than once - #146
Conversation
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
@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 — 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 —
toStandardBodynow wraps the event/octet stream resolvers inresolveStreamBodyOnce, so a secondresolveBody()throws the sameTypeErroras 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 pendingpull()on a dropped queue always settles.stream/cancelis still emitted only oncancelled + 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Calling
resolveBody()twice on a streamed peer body (event stream or octet stream) now throwsTypeError: 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 onServerPeerthe second reader could hang forever, because the queue was dropped without being closed.Fixes
resolveBody()on an event-stream or octet-stream body throws aTypeError, in bothClientPeer(response bodies) andServerPeer(request bodies). The first reader still gets every message.ServerPeernow 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
resolveBody()throws, both before and after the stream ends, and that the first reader still receives every message. They fail againstmain.main.pnpm run checkandpnpm testpass (1336 vitest tests, plus the Bun and Deno suites).🤖 Generated with Claude Code
https://claude.ai/code/session_012g4ovPb64wru9czTYsrwGe