diff --git a/packages/fetch/src/response.test.ts b/packages/fetch/src/response.test.ts index cd7adbc..05f18ff 100644 --- a/packages/fetch/src/response.test.ts +++ b/packages/fetch/src/response.test.ts @@ -1,4 +1,5 @@ import type { StandardResponse } from '@standard-server/core' +import { AsyncIteratorClass } from '@standard-server/shared' import * as Body from './body' import * as Headers from './headers' import { toFetchResponse, toStandardLazyResponse } from './response' @@ -35,6 +36,35 @@ describe('toFetchResponse', () => { expect(toFetchHeadersSpy).toBeCalledTimes(1) expect(toFetchHeadersSpy).toBeCalledWith(toFetchBodySpy.mock.results[0]!.value[1]) }) + + describe('releases the body when the response cannot be built', () => { + it('event-stream body when the status cannot have a body', async () => { + const next = vi.fn(() => new Promise(() => {})) + const cleanup = vi.fn() + + expect(() => toFetchResponse({ status: 204, headers: {}, body: new AsyncIteratorClass(next, cleanup) })).toThrow(TypeError) + + // the event stream starts pulling right away, so it must be cancelled to stop + // the keep-alive interval and release the pending iterator + await vi.waitFor(() => expect(cleanup).toHaveBeenCalledWith({ kind: 'cancelled' })) + expect(next).toHaveBeenCalledTimes(1) + }) + + it('stream body when a header is invalid', () => { + const cancel = vi.fn() + + expect(() => toFetchResponse({ status: 200, headers: { 'x-custom-header': 'a\nb' }, body: new ReadableStream({ cancel }) })).toThrow(TypeError) + expect(cancel).toHaveBeenCalledWith(expect.any(TypeError)) + }) + + it('throws the original error when the stream cannot be cancelled', () => { + const body = new ReadableStream() + body.getReader() + + // a locked stream rejects `cancel()`, which must not surface as an unhandled rejection + expect(() => toFetchResponse({ status: 200, headers: {}, body })).toThrow(/locked/) + }) + }) }) describe('toStandardLazyResponse', () => { diff --git a/packages/fetch/src/response.ts b/packages/fetch/src/response.ts index 050a1e7..bb4de31 100644 --- a/packages/fetch/src/response.ts +++ b/packages/fetch/src/response.ts @@ -11,15 +11,25 @@ export function toFetchResponse( options: ToFetchResponseOptions = {}, ): Response { const [body, standardHeaders] = toFetchBody(standardResponse.body, standardResponse.headers, options) - const response = new Response(body, { - headers: toFetchHeaders(standardHeaders), - status: standardResponse.status, - }) - // not sure why, but some tests (@hono/node-server) fail without pre-accessing body - void response.body + try { + const response = new Response(body, { + headers: toFetchHeaders(standardHeaders), + status: standardResponse.status, + }) - return response + // not sure why, but some tests (@hono/node-server) fail without pre-accessing body + void response.body + + return response + } + catch (error) { + if (body instanceof ReadableStream) { + body.cancel(error).catch(() => {}) + } + + throw error + } } export function toStandardLazyResponse(