{"record":{"id":"cf313e43a947632b","repo":"headroomlabs-ai/headroom","slug":"request-body-must-be-a-json-object-not-type-r","errorCode":null,"errorMessage":"Request body must be a JSON object, not \" + type(result).__name__","messagePattern":"Request body must be a JSON object, not \" \\+ type\\(result\\)\\.__name__","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"headroom/proxy/helpers.py","lineNumber":2519,"sourceCode":"    Clients like OpenAI Codex may send zstd, gzip, or deflate-compressed\n    request bodies.  Starlette's ``request.json()`` does not decompress\n    automatically, causing a UnicodeDecodeError on compressed bytes.\n\n    This helper inspects ``Content-Encoding``, decompresses if needed,\n    then JSON-decodes the result.  It raises ``ValueError`` on any\n    decompression or parse failure so callers can return a clean 400.\n    \"\"\"\n    raw = await _read_request_body_bytes(request)\n\n    # Decode and parse JSON\n    try:\n        text = raw.decode(\"utf-8\")\n    except UnicodeDecodeError as exc:\n        raise ValueError(f\"Request body is not valid UTF-8 (possibly compressed?): {exc}\") from exc\n\n    result = json.loads(text)\n    if not isinstance(result, dict):\n        raise ValueError(\"Request body must be a JSON object, not \" + type(result).__name__)\n\n    # Drop output-only blocks the request schema rejects (see\n    # ``strip_output_only_request_blocks``). Callers of this bytes-less reader\n    # (e.g. the Gemini path) re-serialize ``result`` themselves.\n    if strip_output_only_request_blocks(result.get(\"messages\")):\n        logger.warning(\n            \"removed output-only content block(s) (%s) from request messages \"\n            \"before forwarding (not valid on the request path)\",\n            \",\".join(sorted(OUTPUT_ONLY_REQUEST_BLOCK_TYPES)),\n        )\n\n    return result\n\n\nasync def read_request_json_with_bytes(request: Request) -> tuple[dict[str, Any], bytes]:\n    \"\"\"Read JSON body AND return the original (decompressed) bytes.\n\n    Returned bytes are post-content-decoding (zstd/gzip/deflate/br are","sourceCodeStart":2501,"sourceCodeEnd":2537,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/proxy/helpers.py#L2501-L2537","documentation":"Error \"Request body must be a JSON object, not \" + type(result).__name__\" thrown in headroomlabs-ai/headroom.","triggerScenarios":"Raised when a parsed JSON request body is not an object (e.g. an array or string), which the proxy handler cannot process.","commonSituations":"See trigger scenarios.","solutions":["Send a JSON object (`{...}`) as the request body, not an array or scalar","Check the client code path that serializes the request — it may be double-encoding or sending a list","Verify Content-Type: application/json matches the actual payload"],"exampleFix":null,"handlingStrategy":null,"validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}