Skip to content

Make the deeply nested body test independent of stack size - #3610

Merged
maxisbey merged 1 commit into
mainfrom
3146-nested-body-test
Oct 1, 2026
Merged

maxisbey merged 1 commit into
mainfrom
3146-nested-body-test

Conversation

@maxisbey

@maxisbey maxisbey commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #3146

test_modern_post_with_deeply_nested_body_is_parse_error_not_a_crash posted a well-formed 100,000-deep JSON array and asserted PARSE_ERROR. That only holds where json.loads hits its recursion guard. On CPython 3.14 with a large enough C stack the body parses, the server correctly answers INVALID_REQUEST for a body that isn't a request object, and the assertion fails. The server is right in both cases, so this only changes the test.

What changed

  • The body is now left unterminated (b"[" * 100_000), so it can't parse on any interpreter:
    • RecursionError where the guard trips first
    • JSONDecodeError where the stack is deep enough for the scanner to reach the end of input
  • The handler already maps both to 400 + PARSE_ERROR, so the assertion holds either way.
  • The docstring says that. The test name and assertions are unchanged, and tests/server/mcpserver/test_func_metadata.py already uses an unterminated "[" * 200_000 for the same reason.

How it was checked

  • Reproduced the failure on Linux with CPython 3.14.6 by raising the stack limit (ulimit -s 65536): the old test fails with assert -32600 == -32700, the same as the report. With ulimit -s 8192 it passes.

  • Watched what the handler's json.loads call does with each body on 3.14:

    stack limit old body (closed) new body (unterminated)
    8 MiB RecursionError, -32700 RecursionError, -32700
    16 MiB RecursionError, -32700 RecursionError, -32700
    64 MiB parses, -32600 JSONDecodeError, -32700
  • The new test passes on 3.14 under both 8 MiB and 64 MiB limits.

  • ./scripts/test (full suite, 100% coverage, strict-no-cover), ruff check, ruff format --check and pyright on the file are clean.

  • Not run:

    • macOS, where the issue was reported. The 64 MiB Linux run fails with the same assertion.
    • The test itself on 3.10 to 3.13, which is left to CI. On 3.12 and 3.13 json.loads raises RecursionError for both bodies at either stack limit.

Trade-off

Where the stack is large enough that the guard doesn't trip, the test goes through the JSONDecodeError arm of the except, not the RecursionError one. The RecursionError arm is still the one exercised in CI: the old well-formed body only passed there because the guard trips on every current cell.

AI Disclaimer

The test posted a well-formed 100,000-deep JSON array and asserted
PARSE_ERROR, which only holds where json.loads hits its recursion guard.
On CPython 3.14 with a large enough C stack the body parses, the server
correctly answers INVALID_REQUEST, and the assertion fails.

Leave the body unterminated so it is unparseable on every interpreter:
RecursionError where the guard trips, JSONDecodeError where the scanner
reaches the end of input. The server maps both to 400 + PARSE_ERROR.

Fixes #3146

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

LGTM — the unterminated body cannot parse on any interpreter, and the handler's except (ValueError, RecursionError) at src/mcp/server/_streamable_http_modern.py:403 maps both outcomes to 400 + PARSE_ERROR, so the unchanged assertions hold regardless of stack depth. The inline docstring note is a style nit only.

Extended reasoning...

The change is a two-line, test-only edit to tests/server/test_streamable_http_modern.py: the request body drops its closing brackets and the docstring is reworded. It touches no src/ code and no security-sensitive surface. I confirmed the server handler catches both ValueError (which covers JSONDecodeError) and RecursionError on the same path, so the test's expected 400 + PARSE_ERROR is correct whether the recursion guard trips or the scanner reaches end of input. The same unterminated-bracket pattern already exists in tests/server/mcpserver/test_func_metadata.py, and no CODEOWNERS file covers the path. The only reported finding is a docstring-vs-comment convention nit that does not affect correctness.

Comment on lines +1142 to +1143
"""Unterminated deep nesting makes json.loads raise RecursionError or, where the stack is deep enough
to reach the end of input, JSONDecodeError; an unparseable body either way: 400 + PARSE_ERROR."""

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.

🟡 (optional) Readers of this test get a docstring that explains the non-obvious reason for the body shape instead of a comment on the line that needs it. The docstring at tests/server/test_streamable_http_modern.py:1142-1143 spends its text on why json.loads raises RecursionError versus JSONDecodeError, which is the constraint behind the b"[" * 100_000 body at line 1144, not the behaviour under test. Fix: keep the docstring to the behaviour (unparseable body -> 400 + PARSE_ERROR, SDK-defined) and move the stack-depth rationale to a one-line comment next to line 1144, matching the sibling test in test_func_metadata.py.

Why this was flagged

The convention in .claude/skills/test-quality/SKILL.md:108 says comments live next to the line they explain, not in docstrings, and SKILL.md:16 asks docstrings for 1-2 sentences of behaviour. The new docstring at tests/server/test_streamable_http_modern.py:1142-1143 is a 40-word compound sentence whose first clause is an explanation of interpreter stack behaviour, a constraint on the fixture body at line 1144, not behaviour. When the body is next edited (e.g. someone 'fixes' the missing closing brackets because the test name says deeply nested), the rationale in the docstring is easy to miss and the stack-size dependence from issue #3146 returns. The dismissing finder counted sentences rather than checking what the sentence explains. Remedy: trim the docstring to the behaviour and put the unterminated-body reason as a comment beside line 1144.

Verification: nit. Cosmetic placement issue; nothing fails at runtime. /home/claude/python-sdk/.claude/skills/test-quality/SKILL.md:108 reads "Comments live next to the line they explain, not in docstrings". The docstring at tests/server/test_streamable_http_modern.py:1142-1143 carries the rationale for why line 1144 is b"[" * 100_000 with no closing brackets, placed in the docstring rather than beside it.

@maxisbey
maxisbey merged commit 7fcaad0 into main Oct 1, 2026
36 checks passed
@maxisbey
maxisbey deleted the 3146-nested-body-test branch October 1, 2026 13:24
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.

Deeply-nested-body test fails on macOS: rejected as INVALID_REQUEST (-32600) instead of PARSE_ERROR (-32700)

1 participant