Skip to content

[v1.x] fix(server): read HTTP request bodies with a size limit and bound JSON-RPC batch length - #2717

Merged
maxisbey merged 1 commit into
v1.xfrom
streamable-http-body-limits-v1x
Aug 25, 2026
Merged

[v1.x] fix(server): read HTTP request bodies with a size limit and bound JSON-RPC batch length#2717
maxisbey merged 1 commit into
v1.xfrom
streamable-http-body-limits-v1x

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

v1.x backport of #2698.

WebStandardStreamableHTTPServerTransport (and StreamableHTTPServerTransport, which wraps it) read the whole POST body into memory with req.json() before doing anything with it, and accepted a JSON-RPC batch array of any length. It now reads with a 4 MiB limit by default and answers 413 past that, and caps batch arrays at 100 messages (400 / -32600).

Motivation and Context

Same as #2698: the other body reads on this line already have a ceiling (createMcpExpressApp via express.json(), the SSE transport via raw-body at '4mb', stdio via maxBufferSize); the web-standard transport was the exception.

  • A declared Content-Length over the limit is refused without reading; otherwise reading stops as soon as the limit is crossed, so chunked bodies are covered too. Unchanged when a pre-parsed body is passed as parsedBody (e.g. req.body behind express.json()), which remains the way to opt out of the SDK's read entirely.
  • The limit is configurable with maxRequestBodySize (bytes, default DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB) on the transport options; a non-positive or non-finite value throws a RangeError at construction.
  • A batch array longer than 100 is answered 400 before any element is parsed, on both the read path and a supplied parsedBody.
  • createMcpExpressApp now installs express.json() after the Host header validation, so a request from a disallowed Host is answered 403 without its body being read.

Differences from #2698

  • 1.x has a single body read site: there is no createMcpHandler, toNodeHandler or Hono adapter on this line, so only the transport and createMcpExpressApp change. StreamableHTTPServerTransportOptions is an alias of the web-standard options, so the Node wrapper takes maxRequestBodySize with no code change (one test pins it).
  • The helper module is src/server/requestBody.ts, identical to main's and importable as @modelcontextprotocol/sdk/server/requestBody.js through the existing ./* export; DEFAULT_MAX_REQUEST_BODY_SIZE and readRequestBody are the supported names.
  • createMcpExpressApp on 1.x has no Origin validation or jsonLimit, so the moved line is express.json() after the Host header validation only.
  • The transport's existing catch {} at the read site is kept, so a body read failure is still reported to onerror as "Parse error: Invalid JSON" on this line (main forwards the underlying error); the HTTP response is the same.
  • No changeset (not used on 1.x) and no version bump.

How Has This Been Tested?

New tests mirroring #2698's transport tests (declared and streamed over-limit bodies → 413 without the stream being pulled further; maxRequestBodySize applied on both read paths and validated at construction; 101-message batch → 400 with nothing dispatched, read path and parsedBody), one through StreamableHTTPServerTransport over node:http to pin that the wrapper honours the option, and the Express ordering test. Each fails on v1.x before the change. npm run check, npm test pass.

Breaking Changes

None to the API (one new optional transport option, one new module). Behavioural: POST bodies over 4 MiB get 413 when the transport reads the body itself; batch arrays over 100 entries get 400 (including via parsedBody); createMcpExpressApp answers a disallowed Host with an invalid JSON body 403 rather than 400.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

AI Disclaimer

…und JSON-RPC batch length

Backport of #2698. WebStandardStreamableHTTPServerTransport (and
StreamableHTTPServerTransport, which wraps it) reads the POST body with a
4 MiB limit by default and answers 413 past it, configurable with
maxRequestBodySize; JSON-RPC batch arrays are capped at 100 messages.
createMcpExpressApp runs its Host header validation before express.json().

Differences from #2698: 1.x has a single body read site (no
createMcpHandler, toNodeHandler or hono adapter), so only the transport
and the Express app change; the helper module is src/server/requestBody.ts.
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a33f6d8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@maxisbey
maxisbey marked this pull request as ready for review August 25, 2026 16:21
@maxisbey
maxisbey requested a review from a team as a code owner August 25, 2026 16:21
@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2717

commit: a33f6d8

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline finding, I also examined whether readRequestBody bailing out on an over-limit body without cancelling the stream (it only does reader.releaseLock()) leaks memory — it does not: the unread remainder stays backpressured at the socket rather than buffered in the process, and closing the connection after the 413 is the HTTP server's responsibility, consistent with how express.json/raw-body behave.

Extended reasoning...

One candidate issue was investigated and ruled out this run: the mid-stream too-large path in src/server/requestBody.ts (line 55) returns without calling reader.cancel(), and the Content-Length fast path (line 37) never touches the body at all. Neither causes unbounded memory use — the limit's purpose is to bound what the process buffers, and unread stream data is subject to flow control, not accumulation — and discarding/tearing down the connection after an early 413 is left to the serving runtime, the same contract existing body-limiting middleware relies on. This is recorded as informational context only; the confirmed inline finding on the catch block in webStandardStreamableHttp.ts stands on its own.

Comment on lines 757 to 759
} catch {
this.onerror?.(new Error('Parse error: Invalid JSON'));
return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing (kept by this diff): the bare catch around the body read hard-codes 400/-32700 'Parse error: Invalid JSON' for all failures, including stream/read errors from readRequestBody, without discriminating the thrown cause — the repo review instructions (Recurring Catches: Error Handling) explicitly require flagging catch-alls that emit client-fault JSON-RPC codes for server/transport-internal failures.

Extended reasoning...

A client's connection drops (or the upstream stream errors) mid-upload of a POST body. readRequestBody propagates the stream failure, the catch at lines 757-759 swallows it, and the server answers 400 with JSON-RPC code -32700 'Parse error: Invalid JSON'. The client is told its request was malformed and may reformat/retry the same payload pointlessly, and onerror receives a fabricated parse error instead of the real network/stream error, hiding the actual cause from server operators.

Verification: pre-existing. The bare catch is real at HEAD src/server/webStandardStreamableHttp.ts:750-760: const body = await readRequestBody(req, this._maxRequestBodySize); ... rawMessage = JSON.parse(body.text); } catch { this.onerror?.(new Error('Parse error: Invalid JSON')); return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON'); }. src/server/requestBody.ts's readRequestBody expli

@maxisbey
maxisbey merged commit a9f6eb7 into v1.x Aug 25, 2026
12 checks passed
@maxisbey
maxisbey deleted the streamable-http-body-limits-v1x branch August 25, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants