Skip to content

Commit 580c3cd

Browse files
GordonGordon
authored andcommitted
fix: make deeply-nested-body test platform-independent (#3146)
Since CPython 3.12 the C json scanner guards recursion by remaining C-stack headroom rather than a fixed depth, so whether a 100k-deep body raises RecursionError (-> PARSE_ERROR) or parses into a giant list that fails request validation (-> INVALID_REQUEST) depends on the thread's stack size. macOS threads run with a smaller default stack, which is why the test failed there while passing on Ubuntu and Windows. Split the test in two: the nested-body test now asserts a 400 with either rejection code (both are correct; neither is a crash), and a new monkeypatch-based test pins the RecursionError -> PARSE_ERROR mapping deterministically on every platform. Fixes #3146
1 parent 3a6f299 commit 580c3cd

1 file changed

Lines changed: 27 additions & 2 deletions

File tree

tests/server/test_streamable_http_modern.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,12 +1008,37 @@ async def broken_list(ctx: ServerRequestContext, params: PaginatedRequestParams
10081008
assert "Mcp-Param header validation skipped: the tools/list listing failed" in caplog.text
10091009

10101010

1011-
async def test_modern_post_with_deeply_nested_body_is_parse_error_not_a_crash() -> None:
1012-
"""Deep nesting makes json.loads raise RecursionError; still an unparseable body: 400 + PARSE_ERROR."""
1011+
async def test_modern_post_with_deeply_nested_body_is_rejected_not_a_crash() -> None:
1012+
"""A deeply nested body is rejected with a 400, never a crash.
1013+
1014+
Which JSON-RPC code it gets is platform-dependent: since CPython 3.12 the C
1015+
json scanner guards recursion by actual C-stack headroom, so depending on
1016+
the thread's stack size the body either fails to parse (RecursionError ->
1017+
PARSE_ERROR) or parses into a giant list that then fails request
1018+
validation (-> INVALID_REQUEST). Both are correct rejections; the
1019+
deterministic PARSE_ERROR mapping is covered by the monkeypatch test below.
1020+
"""
10131021
body = b"[" * 100_000 + b"]" * 100_000
10141022
async with _asgi_client(_x_mcp_server()) as http:
10151023
response = await http.post("/mcp", content=body, headers={"content-type": "application/json"})
10161024
assert response.status_code == 400
1025+
assert response.json()["error"]["code"] in (PARSE_ERROR, INVALID_REQUEST)
1026+
1027+
1028+
async def test_modern_post_recursion_error_during_parse_is_parse_error(monkeypatch: pytest.MonkeyPatch) -> None:
1029+
"""RecursionError raised while parsing the body maps to 400 + PARSE_ERROR (deterministically)."""
1030+
real_loads = json.loads
1031+
body = b"[" * 50 + b"]" * 50
1032+
1033+
def _raise_for_request_body(s: Any, *args: Any, **kwargs: Any) -> Any:
1034+
if s == body: # `json` here is the stdlib module; only fail the request-body parse.
1035+
raise RecursionError("maximum recursion depth exceeded")
1036+
return real_loads(s, *args, **kwargs)
1037+
1038+
monkeypatch.setattr("mcp.server._streamable_http_modern.json.loads", _raise_for_request_body)
1039+
async with _asgi_client(_x_mcp_server()) as http:
1040+
response = await http.post("/mcp", content=body, headers={"content-type": "application/json"})
1041+
assert response.status_code == 400
10171042
assert response.json()["error"]["code"] == PARSE_ERROR
10181043

10191044

0 commit comments

Comments
 (0)