From 49cb43e24aa38ecea46e4e734eb600ddbd6ad158 Mon Sep 17 00:00:00 2001 From: yuxuan <55852668+littleKitchen@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:51:10 -0700 Subject: [PATCH] fix(server): read request bodies where Request.body is not implemented Firefox does not implement `Request.body` yet, so `readRequestBody` threw on `request.body.getReader()` and every POST to a browser-hosted server was answered with a JSON-RPC parse error. When `request.body` is undefined, read the body with `arrayBuffer()` and check its size against `maxBytes` afterwards, so oversized bodies still get `413`. The streaming path is unchanged for runtimes that expose the stream. Fixes #3025 Co-Authored-By: Claude Opus 5.5 --- .../read-body-without-request-body-stream.md | 5 +++++ packages/server/src/server/requestBody.ts | 9 ++++++++- .../server/test/server/createMcpHandler.test.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 .changeset/read-body-without-request-body-stream.md diff --git a/.changeset/read-body-without-request-body-stream.md b/.changeset/read-body-without-request-body-stream.md new file mode 100644 index 0000000000..2376b64051 --- /dev/null +++ b/.changeset/read-body-without-request-body-stream.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +`readRequestBody`, and with it `createMcpHandler` and the Streamable HTTP transport, now reads request bodies in runtimes that have no `Request.body` stream, such as Firefox. Previously every POST there was answered `400` with a JSON-RPC parse error (`-32700`). In those runtimes the body is read whole and then checked against `maxRequestBodySize`, so oversized bodies are still answered `413`. diff --git a/packages/server/src/server/requestBody.ts b/packages/server/src/server/requestBody.ts index a11e8d99ed..f583f94f90 100644 --- a/packages/server/src/server/requestBody.ts +++ b/packages/server/src/server/requestBody.ts @@ -28,7 +28,9 @@ export function resolveMaxRequestBodySize(value: number | undefined): number { * Reads a request body as text, up to `maxBytes` (default * {@linkcode DEFAULT_MAX_REQUEST_BODY_SIZE}). A declared `Content-Length` over the * limit is refused without reading anything; otherwise the read stops as soon as - * more than the limit has arrived. Stream failures propagate. + * more than the limit has arrived. Where the runtime has no `Request.body` stream + * (Firefox), the body is read whole and then checked against the limit. Stream + * failures propagate. */ export async function readRequestBody( request: Request, @@ -40,6 +42,11 @@ export async function readRequestBody( if (request.body === null) { return { tooLarge: false, text: '' }; } + // Firefox does not implement `Request.body` yet, though `arrayBuffer()` works. + if (request.body === undefined) { + const bytes = await request.arrayBuffer(); + return bytes.byteLength > maxBytes ? { tooLarge: true } : { tooLarge: false, text: new TextDecoder().decode(bytes) }; + } const reader = request.body.getReader(); const decoder = new TextDecoder(); let received = 0; diff --git a/packages/server/test/server/createMcpHandler.test.ts b/packages/server/test/server/createMcpHandler.test.ts index a1b9ce7986..5536ef7606 100644 --- a/packages/server/test/server/createMcpHandler.test.ts +++ b/packages/server/test/server/createMcpHandler.test.ts @@ -623,6 +623,20 @@ describe('createMcpHandler — stateless legacy fallback (the default)', () => { } }); + it('reads the body where the runtime has no Request.body stream (Firefox), keeping the size limit', async () => { + const { factory, state } = testFactory(); + const handler = createMcpHandler(factory); + const withoutBodyStream = (request: Request): Request => Object.defineProperty(request, 'body', { value: undefined }); + + const served = await handler.fetch(withoutBodyStream(postRequest(modernToolsCall('echo', { text: 'no body stream' })))); + expect(served.status).toBe(200); + expect(await served.text()).toContain('no body stream'); + + const refused = await handler.fetch(withoutBodyStream(postRequest('x'.repeat(4 * 1024 * 1024 + 1)))); + expect(refused.status).toBe(413); + expect(state.contexts).toHaveLength(1); + }); + it('still serves the modern path on the same endpoint (one factory, both legs)', async () => { const { factory, state } = testFactory(); const handler = createMcpHandler(factory);