{"record":{"id":"7d62abe093649e31","repo":"can1357/oh-my-pi","slug":"json-body-must-be-an-object","errorCode":null,"errorMessage":"json body must be an object","messagePattern":"json body must be an object","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"python/robomp/src/proxy/server.py","lineNumber":658,"sourceCode":"    @app.get(\"/gh/v1/pr_reviews\")\n    async def list_pr_reviews(request: Request, repo: str, pr_number: int) -> JSONResponse:\n        await _authenticate(request)\n        github: GitHubClient = request.app.state.github\n        try:\n            items = await github.list_pr_reviews(repo, pr_number)\n        except GitHubError as exc:\n            return _gh_error_response(exc)\n        return JSONResponse({\"items\": [_serialize(r) for r in items]})\n\n    # ---- writes ----\n    async def _json_body(request: Request) -> dict[str, Any]:\n        await _authenticate(request)\n        try:\n            data = await request.json()\n        except Exception as exc:\n            raise HTTPException(400, f\"invalid json: {exc}\") from exc\n        if not isinstance(data, dict):\n            raise HTTPException(400, \"json body must be an object\")\n        return data\n\n    @app.post(\"/gh/v1/post_comment\")\n    async def post_comment(request: Request) -> JSONResponse:\n        data = await _json_body(request)\n        repo = _require_str(data.get(\"repo\"), \"repo\")\n        number = _require_int(data.get(\"number\"), \"number\")\n        body = _require_str(data.get(\"body\"), \"body\")\n        github: GitHubClient = request.app.state.github\n        try:\n            info = await github.post_comment(repo, number, body)\n        except GitHubError as exc:\n            return _gh_error_response(exc)\n        return JSONResponse(_serialize(info))\n\n    @app.post(\"/gh/v1/open_pull_request\")\n    async def open_pull_request(request: Request) -> JSONResponse:\n        data = await _json_body(request)","sourceCodeStart":640,"sourceCodeEnd":676,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/proxy/server.py#L640-L676","documentation":"The gh-proxy server's `_json_body` helper parses the request body of GitHub proxy endpoints and requires the parsed JSON to be a JSON object. If the client sends valid JSON that is not an object (e.g. an array, string, number, or `null`), the proxy rejects it with HTTP 400 so downstream handlers never see unusable input. This is an input-contract error: the endpoint expects `Content-Type: application/json` with a top-level `{...}` body.","triggerScenarios":"POSTing to /gh/v1/post_comment, open_pull_request, request_reviewers, add_issue_labels, remove_issue_label, or submit_pr_review with a body like `\"text\"`, `[1,2]`, `42`, or `null` instead of `{\"repo\": ...}`. Also occurs when the body is valid JSON but the client sends a bare quoted string (double-encoded JSON) or when middleware/serialization accidentally wraps the payload.","commonSituations":"Double-JSON-encoding (JSON.stringify applied twice), sending a JSON array of fields instead of an object, sending `null` for an optional-empty body, or a misconfigured HTTP client that posts form data that happens to parse as a scalar.","solutions":["Inspect the exact request body being sent and ensure the top level is a JSON object: `{\"repo\": \"owner/name\", ...}`","Fix double-encoding: call JSON.stringify once on the object, not on an already-serialized string","Ensure the Content-Type header is application/json so the client serializes the object correctly","If the body may be empty, send `{}` rather than `null` or an empty string"],"exampleFix":"// before\nawait client.post(url, content=JSON.stringify(JSON.stringify(payload)))\n// after\nawait client.post(url, content=JSON.stringify(payload), headers={\"content-type\": \"application/json\"})","handlingStrategy":"validation","validationCode":"def ensure_json_object(payload):\n    if not isinstance(payload, dict):\n        raise ValueError(f\"body must be a JSON object, got {type(payload).__name__}\")\n    return payload\nensure_json_object(payload)  # call before POSTing","typeGuard":"def is_json_object(v: object) -> TypeGuard[dict]:\n    return isinstance(v, dict)","tryCatchPattern":"try:\n    resp = post_json(url, payload)\nexcept HTTPError as e:\n    if e.response.status_code == 400 and \"json body must be an object\" in e.response.text:\n        # fix payload shape: wrap scalars/arrays into an object before retrying\n        payload = {\"data\": payload}\n        resp = post_json(url, payload)\n    else:\n        raise","preventionTips":["Always send a top-level object; wrap auxiliary data in a field if needed","Stringify once — never double-encode JSON","Set Content-Type: application/json and use your client's JSON serializer","Send {} instead of null/empty string for empty bodies"],"tags":["http","validation","client-error"],"backgroundTag":"json-body-not-an-object","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}