Skip to content

[v1.x] Add Streamable HTTP request body limits#3101

Merged
maxisbey merged 1 commit into
v1.xfrom
agent/v1-streamable-http-request-limit
Jul 16, 2026
Merged

[v1.x] Add Streamable HTTP request body limits#3101
maxisbey merged 1 commit into
v1.xfrom
agent/v1-streamable-http-request-limit

Conversation

@Kludex

@Kludex Kludex commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

  • backport the configurable max_request_body_size option to the V1 Streamable HTTP server
  • apply the default 4 MiB limit to declared and streamed POST bodies and return HTTP 413 when exceeded
  • expose the setting through FastMCP and document how to configure larger messages
  • consolidate streamed chunks before replaying the bounded request body

Backport of #3095.

Validation

  • ./scripts/test — 1,156 passed, 95 skipped, 1 xfailed, 100% coverage
  • uv run --frozen pre-commit run --all-files

@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3101.mcp-python-docs.pages.dev
Deployment https://2ed8ab93.mcp-python-docs.pages.dev
Commit d758cd5
Triggered by @Kludex
Updated 2026-07-16 07:50:36 UTC

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Beyond the inline pragma nit, a few other candidates were examined and ruled out: (1) the middleware buffering the POST body before origin/session validation — the buffer is capped at max_request_body_size itself, so memory exposure is bounded by design; (2) the plain-text 413 body being inconsistent with the manager's JSON-RPC error responses — the rejection happens before JSON-RPC parsing, so there is no request id to correlate and a plain HTTP response is appropriate; (3) FastMCP not validating max_request_body_size at construction — the manager's constructor validates it when streamable_http_app() runs, so the error still surfaces, just later.

Extended reasoning...

Bugs-found run: one nit (stale pragma) posted inline. This note records the candidate issues that finder agents raised and verifiers refuted, plus my own check that the transport's DNS-rebinding/origin validation is header-only, so the middleware's bounded body buffering ahead of it does not weaken that protection. No verdict on overall approval is implied — the PR adds a new middleware to the streamable HTTP request path and warrants human review as a transport behavior change.

Comment on lines +1495 to +1501
def test_streamable_http_app_passes_the_configured_request_body_limit_to_its_manager() -> None:
"""SDK-defined: FastMCP forwards its public request-body setting to the Streamable HTTP manager."""
mcp = FastMCP(max_request_body_size=8)

mcp.streamable_http_app()

assert mcp.session_manager.max_request_body_size == 8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 The new test test_streamable_http_app_passes_the_configured_request_body_limit_to_its_manager accesses mcp.session_manager, which makes the # pragma: no cover on the return self._session_manager line (src/mcp/server/fastmcp/server.py:280) stale — the line is now executed by the suite but still excluded from coverage measurement. Since this PR already removes newly-stale pragmas elsewhere (the mock_receive functions in test_streamable_http_manager.py), drop this one too; the None-check raise branch above it remains genuinely uncovered and keeps its pragma.

Extended reasoning...

What the issue is

The FastMCP.session_manager property in src/mcp/server/fastmcp/server.py ends with:

return self._session_manager  # pragma: no cover

Before this PR, no test in the suite accessed the session_manager property directly, so this pragma accurately marked an unexecuted line. This PR adds test_streamable_http_app_passes_the_configured_request_body_limit_to_its_manager (tests/server/fastmcp/test_server.py:1495-1501), which is now the only test that reads mcp.session_manager — and it does so after calling streamable_http_app(), so the None guard does not fire and the return line executes.

The specific code path

  1. FastMCP(max_request_body_size=8) is constructed with self._session_manager = None.
  2. mcp.streamable_http_app() runs; the lazy-init branch creates the StreamableHTTPSessionManager and assigns it to self._session_manager.
  3. The test's assertion evaluates mcp.session_manager, entering the property. self._session_manager is None is False, so the raise RuntimeError(...) branch is skipped and return self._session_manager at line 280 executes.

So line 280 is now exercised on every test run while still being excluded from coverage measurement by the pragma.

Why nothing catches this

This repo enforces fail_under = 100 with "pragma: no cover" in exclude_lines (pyproject.toml). Coverage.py silently ignores excluded lines whether or not they execute — it never fails on an excluded-but-executed line — so CI stays green and the stale pragma persists unnoticed.

Impact

Zero runtime impact; this is purely coverage hygiene. The cost is that a genuinely-covered line is invisible to coverage reporting, which erodes the value of the 100%-coverage convention: pragmas are supposed to mark only lines the suite truly cannot reach. Notably, this PR itself follows that convention elsewhere — it removes the now-stale # pragma: no cover comments from several mock_receive functions in tests/server/test_streamable_http_manager.py that became covered by the same change — so leaving this one behind is an internal inconsistency within the PR.

How to fix

Delete the trailing comment on line 280:

return self._session_manager

The raise RuntimeError branch at lines 273-279 (the self._session_manager is None case) remains genuinely unexecuted by the suite and should keep its pragma.

Step-by-step proof

  1. Run grep -rn "\.session_manager" tests/ — the only hit is the new test at tests/server/fastmcp/test_server.py:1501, confirming the pragma was accurate before this PR.
  2. Run the new test: FastMCP(max_request_body_size=8)streamable_http_app() sets _session_managermcp.session_manager.max_request_body_size == 8 passes, which requires the property to have returned self._session_manager — i.e., line 280 executed.
  3. Run coverage: line 280 shows as excluded (not counted covered or missing) because of the pragma, even though step 2 proved it executes. Removing the pragma and re-running keeps coverage at 100%, confirming the pragma is stale and its removal is safe.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 5 files

Re-trigger cubic

@maxisbey maxisbey merged commit 5f0b6af into v1.x Jul 16, 2026
31 checks passed
@maxisbey maxisbey deleted the agent/v1-streamable-http-request-limit branch July 16, 2026 08:54
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