From 766636967338b7d4e783f80f8f8cd05cdef2faea Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 29 Sep 2026 15:38:50 +0700 Subject: [PATCH] fix(peer): release the request body when the signal is already aborted ClientPeer.request() checked the signal before transmitRequest ran, so an already-aborted signal rejected the request without cancelling a ReadableStream body or returning an async iterator body. The check now runs at the start of transmitRequest, so this case goes through the same cleanup as every other exit before the body is sent. --- packages/peer/src/client.test.ts | 29 +++++++++++++++++++++++++++++ packages/peer/src/client.ts | 4 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/peer/src/client.test.ts b/packages/peer/src/client.test.ts index 1c710e95..5ebc0d8c 100644 --- a/packages/peer/src/client.test.ts +++ b/packages/peer/src/client.test.ts @@ -243,6 +243,35 @@ describe('clientPeer', () => { ).rejects.toThrow('pre-aborted') }) + it('cancels an octet-stream request body if signal already aborted', async () => { + const controller = new AbortController() + const error = new Error('pre-aborted') + controller.abort(error) + const cancel = vi.fn() + + await expect( + peer.request(makeRequest({ body: new ReadableStream({ cancel }), signal: controller.signal })), + ).rejects.toBe(error) + await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce()) + expect(cancel).toHaveBeenCalledWith(error) + expect(send).not.toHaveBeenCalled() + expect(getPeerSize(peer)).toEqual(0) + }) + + it('returns an event-stream request body if signal already aborted', async () => { + const controller = new AbortController() + controller.abort(new Error('pre-aborted')) + const cleanup = vi.fn() + + await expect( + peer.request(makeRequest({ body: new AsyncIteratorClass(() => new Promise(() => {}), cleanup), signal: controller.signal })), + ).rejects.toThrow('pre-aborted') + await vi.waitFor(() => expect(cleanup).toHaveBeenCalledOnce()) + expect(cleanup).toHaveBeenCalledWith({ kind: 'cancelled' }) + expect(send).not.toHaveBeenCalled() + expect(getPeerSize(peer)).toEqual(0) + }) + it('sends abort message when signal aborts after request sent', async () => { const controller = new AbortController() const promise = peer.request(makeRequest({ signal: controller.signal })) diff --git a/packages/peer/src/client.ts b/packages/peer/src/client.ts index a93cbbe6..a7e9f3a8 100644 --- a/packages/peer/src/client.ts +++ b/packages/peer/src/client.ts @@ -37,8 +37,6 @@ export class ClientPeer { request(request: StandardRequest): Promise { return new Promise((resolve, reject) => { const signal = request.signal - throwIfAborted(signal) - const id = this.idGenerator.generate() const state: ClientPeerRequestStateInternal = { resolve, reject } this.requests.set(id, state) @@ -71,6 +69,8 @@ export class ClientPeer { let failure: unknown try { + throwIfAborted(request.signal) + const encodedAtomicBody = await encodeAtomicStandardBody(request.body, request.headers) // signal can be aborted during encode