Make the deeply nested body test independent of stack size - #3610
Conversation
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
There was a problem hiding this comment.
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.
| """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.""" |
There was a problem hiding this comment.
🟡 (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.
Fixes #3146
test_modern_post_with_deeply_nested_body_is_parse_error_not_a_crashposted a well-formed 100,000-deep JSON array and assertedPARSE_ERROR. That only holds wherejson.loadshits its recursion guard. On CPython 3.14 with a large enough C stack the body parses, the server correctly answersINVALID_REQUESTfor 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
b"[" * 100_000), so it can't parse on any interpreter:RecursionErrorwhere the guard trips firstJSONDecodeErrorwhere the stack is deep enough for the scanner to reach the end of inputPARSE_ERROR, so the assertion holds either way.tests/server/mcpserver/test_func_metadata.pyalready uses an unterminated"[" * 200_000for 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 withassert -32600 == -32700, the same as the report. Withulimit -s 8192it passes.Watched what the handler's
json.loadscall does with each body on 3.14:RecursionError, -32700RecursionError, -32700RecursionError, -32700RecursionError, -32700JSONDecodeError, -32700The 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 --checkandpyrighton the file are clean.Not run:
json.loadsraisesRecursionErrorfor 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
JSONDecodeErrorarm of theexcept, not theRecursionErrorone. TheRecursionErrorarm 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