diff --git a/.changeset/mighty-worlds-smoke.md b/.changeset/mighty-worlds-smoke.md new file mode 100644 index 00000000000..3ab3aa5e7c4 --- /dev/null +++ b/.changeset/mighty-worlds-smoke.md @@ -0,0 +1,7 @@ +--- +'@whatwg-node/node-fetch': patch +'@whatwg-node/server': patch +'@whatwg-node/fetch': patch +--- + +Fixes the `TypeError: bodyInit.stream is not a function` error thrown when `@whatwg-node/server` is used with `node:http2` and attempts the incoming HTTP/2 request to parse with `Request.json`, `Request.text`, `Request.formData`, or `Request.blob` methods. diff --git a/packages/node-fetch/src/Body.ts b/packages/node-fetch/src/Body.ts index 53d1a581bbf..cef8a55dc03 100644 --- a/packages/node-fetch/src/Body.ts +++ b/packages/node-fetch/src/Body.ts @@ -508,7 +508,7 @@ function isFormData(value: any): value is FormData { } function isBlob(value: any): value is Blob { - return value?.stream != null; + return value?.stream != null && typeof value.stream === 'function'; } function isURLSearchParams(value: any): value is URLSearchParams { diff --git a/packages/server/test/http2.spec.ts b/packages/server/test/http2.spec.ts index ad472f655f5..e59bc0bf497 100644 --- a/packages/server/test/http2.spec.ts +++ b/packages/server/test/http2.spec.ts @@ -27,13 +27,22 @@ describeIf(!globalThis.Bun && !globalThis.Deno)('http2', () => { runTestsForEachFetchImpl((_, { createServerAdapter }) => { it('should support http2 and respond as expected', async () => { - let calledRequest: Request | undefined; - const handleRequest = jest.fn((_request: Request) => { - calledRequest = _request; - return new Response('Hey there!', { - status: 418, - headers: { 'x-is-this-http2': 'yes', 'content-type': 'text/plain;charset=UTF-8' }, - }); + const handleRequest = jest.fn(async (request: Request) => { + return Response.json( + { + body: await request.json(), + headers: Object.fromEntries(request.headers), + method: request.method, + url: request.url, + }, + { + headers: { + 'x-is-this-http2': 'yes', + 'content-type': 'text/plain;charset=UTF-8', + }, + status: 418, + }, + ); }); const adapter = createServerAdapter(handleRequest); @@ -49,11 +58,16 @@ describeIf(!globalThis.Bun && !globalThis.Deno)('http2', () => { const req = client.request({ [constantsHttp2.HTTP2_HEADER_METHOD]: 'POST', [constantsHttp2.HTTP2_HEADER_PATH]: '/hi', + [constantsHttp2.HTTP2_HEADER_CONTENT_TYPE]: 'application/json', }); + req.write(JSON.stringify({ hello: 'world' })); + req.end(); + const receivedNodeRequest = await new Promise<{ headers: Record; data: string; + status?: number | undefined; }>((resolve, reject) => { req.once( 'response', @@ -68,7 +82,8 @@ describeIf(!globalThis.Bun && !globalThis.Deno)('http2', () => { req.on('end', () => { resolve({ headers, - data, + data: JSON.parse(data), + status: headers[':status'], }); }); }, @@ -77,18 +92,25 @@ describeIf(!globalThis.Bun && !globalThis.Deno)('http2', () => { }); expect(receivedNodeRequest).toMatchObject({ - data: 'Hey there!', + data: { + body: { + hello: 'world', + }, + method: 'POST', + url: expect.stringMatching(/^http:\/\/localhost:\d+\/hi$/), + headers: { + 'content-type': 'application/json', + }, + }, headers: { ':status': 418, 'content-type': 'text/plain;charset=UTF-8', 'x-is-this-http2': 'yes', }, + status: 418, }); await new Promise(resolve => req.end(resolve)); - - expect(calledRequest?.method).toBe('POST'); - expect(calledRequest?.url).toMatch(/^http:\/\/localhost:\d+\/hi$/); }); }); });