ADR-1075: MCP HTTP transport POST /v1/score body-validation edge cases¶
- Status: Accepted
- Date: 2026-06-06
- Deciders: Lusoris
- Tags:
mcp,security,correctness,http,fork-local
Context¶
Two correctness bugs were found in _handle_score in mcp-server/vmaf-mcp/src/vmaf_mcp/http_transport.py:
Bug A — chunked oversize body mis-reported as 400 instead of 413. aiohttp.web.HTTPRequestEntityTooLarge is a subclass of both Exception and aiohttp.web.Response. When a chunked (no Content-Length) request body exceeds the client_max_size limit set on the Application, aiohttp raises HTTPRequestEntityTooLarge inside request.json(). The bare except Exception at that call site caught the exception and re-wrapped it as a 400 "invalid JSON" response rather than propagating the 413. Callers observing 400 cannot distinguish a syntax error from an oversize body; the HTTP spec mandates 413 for the latter.
Bug B — JSON null body causes uncaught TypeError → uncontrolled 500. request.json() succeeds for any valid JSON value, including the JSON literal null (Python None). The next statement performs membership tests f not in body on the returned value. When body is None this raises TypeError: argument of type 'NoneType' is not a container or iterable, which propagated as an unhandled exception through the aiohttp framework — producing a plain-text 500 with no request_id field and the wrong Content-Type header. The same issue affected JSON integers, booleans, and floating-point values as the body.
Both bugs were identified through systematic edge-case coverage testing (ADR-0108 scope: malformed JSON-RPC, oversize payload, transport-specific errors).
Decision¶
Fix _handle_score to handle both cases correctly:
-
Add
except aiohttp.web.HTTPRequestEntityTooLarge: raisebefore the genericexcept Exceptionblock so that chunked oversize bodies propagate as 413 (aiohttp converts the exception to the correct wire response) rather than being swallowed as 400. -
After
request.json()returns, checkisinstance(body, dict)and return a structured 400 response (withrequest_id) when the body is not a JSON object.
Update docs/mcp/http-transport.md to document the 413 and 401 responses in the error-response table.
Add nine regression tests in mcp-server/vmaf-mcp/tests/test_mcp_http_edge_cases_adr1075.py covering: both bugs, GET on /v1/score (→ 405), array/integer/string/null JSON bodies, empty body, and concurrent request ID uniqueness.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
Wrap entire handler in try/except | Single catch point | Masks other genuine 500 errors; violates narrow-exception principle | Too broad |
Validate Content-Type: application/json header before parsing | Fails fast for non-JSON requests | Does not protect against valid JSON of wrong type; clients may omit Content-Type | Incomplete |
| Let aiohttp default 500 handler emit the error | Zero code change | No request_id in the response body; plain-text 500 breaks clients that expect JSON | Unacceptable for an API |
Consequences¶
- Positive: chunked oversize bodies now return correct 413; non-dict JSON bodies return structured 400 with
request_id; both are testable via the new regression suite. - Negative: none; both changes are narrowly scoped to the affected code path.
- Neutral / follow-ups:
docs/mcp/http-transport.mdnow documents 413 and 401 in the error-response table for/v1/score.
References¶
mcp-server/vmaf-mcp/src/vmaf_mcp/http_transport.py—_handle_scorefunction.mcp-server/vmaf-mcp/tests/test_mcp_http_edge_cases_adr1075.py— regression tests.- ADR-0967 — original security hardening (auth + body limit + bind default).
- ADR-0701 — HTTP transport foundation.
- ADR-0108 — six-deliverables rule.