Make the deeply nested body test independent of stack size

Merged
MA
15d ago
+3-2

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 limitold body (closed)new body (unterminated)
    8 MiBRecursionError, -32700RecursionError, -32700
    16 MiBRecursionError, -32700RecursionError, -32700
    64 MiBparses, -32600JSONDecodeError, -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