FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix: make deeply-nested-body test platform-independent (fixes #3146) by g0rdonL · Pull Request #3147 · modelcontextprotocol/python-sdk · GitHub

Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (1) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
42 changes: 40 additions & 2 deletions tests/server/test_streamable_http_modern.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import json
import logging
import sys
from collections.abc import Callable
from typing import Any

Expand Down Expand Up @@ -1008,12 +1009,49 @@ async def broken_list(ctx: ServerRequestContext, params: PaginatedRequestParams
assert "Mcp-Param header validation skipped: the tools/list listing failed" in caplog.text


async def test_modern_post_with_deeply_nested_body_is_parse_error_not_a_crash() -> None:
"""Deep nesting makes json.loads raise RecursionError; still an unparseable body: 400 + PARSE_ERROR."""
async def test_modern_post_with_deeply_nested_body_is_rejected_not_a_crash() -> None:
"""A deeply nested body is rejected with a 400, never a crash.

Which JSON-RPC code it gets is platform-dependent on CPython 3.14+: the C
json scanner there guards recursion by actual C-stack headroom, so whether
a 100k-deep body parses depends on the parsing thread's stack size and the
per-level cost on that architecture (~112 bytes/level measured on arm64,
~129 on x86_64). It either fails to parse (RecursionError -> PARSE_ERROR)
or parses into a giant list that then fails request validation
(-> INVALID_REQUEST). CPython gives non-main threads a 16 MiB stack on
macOS (THREAD_STACK_SIZE in thread_pthread.h) vs 8-ish MiB defaults
elsewhere, which is why macOS parses the body and answers INVALID_REQUEST.
Below 3.14 the recursion guard ignores stack size and the 100k-deep body
always fails to parse, so the exact PARSE_ERROR code is asserted there.
Both codes are correct rejections; the deterministic RecursionError ->
PARSE_ERROR mapping is covered by the monkeypatch test below.
"""
body = b"[" * 100_000 + b"]" * 100_000
async with _asgi_client(_x_mcp_server()) as http:
response = await http.post("/mcp", content=body, headers={"content-type": "application/json"})
assert response.status_code == 400
allowed_codes: tuple[int, ...]
if sys.version_info >= (3, 14): # pragma: lax no cover
allowed_codes = (PARSE_ERROR, INVALID_REQUEST)
else: # pragma: lax no cover
allowed_codes = (PARSE_ERROR,)
assert response.json()["error"]["code"] in allowed_codes


async def test_modern_post_recursion_error_during_parse_is_parse_error(monkeypatch: pytest.MonkeyPatch) -> None:
"""RecursionError raised while parsing the body maps to 400 + PARSE_ERROR (deterministically)."""
real_loads = json.loads
body = b"[" * 50 + b"]" * 50

def _raise_for_request_body(s: Any, *args: Any, **kwargs: Any) -> Any:
if s == body: # `json` here is the stdlib module; only fail the request-body parse.
raise RecursionError("maximum recursion depth exceeded")
return real_loads(s, *args, **kwargs)

monkeypatch.setattr("mcp.server._streamable_http_modern.json.loads", _raise_for_request_body)
async with _asgi_client(_x_mcp_server()) as http:
response = await http.post("/mcp", content=body, headers={"content-type": "application/json"})
assert response.status_code == 400
assert response.json()["error"]["code"] == PARSE_ERROR


Expand Down
Loading

Back | FazBrowse Home | New Git URL