fix(server): cap JSON nesting depth in AnyCodable (#462)
AnyCodable.init(from:) recursed once per level of JSON nesting with no depth limit, and RawJSON decodes through it. RawJSON is the declared type of three request fields an unauthenticated caller fully controls: tools[].function.parameters and response_format.json_schema.schema on /v1/chat/completions, and text.format.schema on /v1/responses. Foundation's JSON scanner only rejects nesting at ~512 levels, far deeper than the stack budget of the cooperative-pool thread the handler decodes on. Depths between roughly 160 and 511 therefore passed the scanner and exhausted the stack, aborting the whole process with SIGBUS -- a ~1.3 KB POST killed the server and every in-flight request with it, and under launchd KeepAlive the server restart-looped for as long as the sender repeated. The crash happened during body decoding, before the handler ran, so --token did not protect against it either. The subtlety, and the reason the three candidate PRs (#467, #468, #469) were not taken: a bare depth `guard` is not sufficient. init(from:) probes its two container branches with `try?`, which swallows the thrown depth error, sets the subtree to nil, and answers 200 with the caller's schema silently truncated. That variant was built and measured -- it stops the crash and turns it into silent data loss. (#467 and #468 additionally do not compile: their tests name AnyCodable, which is internal to ApfelCore while Tests/apfelTests imports it without @testable.) So the depth failure is thrown as a real DecodingError -- so every existing `catch is DecodingError`, ours and downstream consumers', still sees it and still produces a 400 with no new error plumbing -- carrying a private NestingLimitExceeded marker in underlyingError so the container probes can tell "not that type" from "too deep" and re-throw only the latter. Verified against the live server, all six deep-nesting cases: entry point depth 200 depth 400 tools[].function.parameters 400, up 400, up response_format.json_schema.schema 400, up 400, up text.format.schema (/v1/responses) 400, up 400, up A normal nested tool schema is unaffected: 200, finish_reason tool_calls. 1056 unit tests pass. Closes #462 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ccLBbEaVVd4sJyUd5wyhA
A
Arthur Ficial committed
e511da21162d5042723228e46f1b4b219341d9ba
Parent: 7cbafa7