can1357/oh-my-pi · error · HTTPException
json body must be an object
Error message
json body must be an object
What it means
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.
Source
Thrown at python/robomp/src/proxy/server.py:658
@app.get("/gh/v1/pr_reviews")
async def list_pr_reviews(request: Request, repo: str, pr_number: int) -> JSONResponse:
await _authenticate(request)
github: GitHubClient = request.app.state.github
try:
items = await github.list_pr_reviews(repo, pr_number)
except GitHubError as exc:
return _gh_error_response(exc)
return JSONResponse({"items": [_serialize(r) for r in items]})
# ---- writes ----
async def _json_body(request: Request) -> dict[str, Any]:
await _authenticate(request)
try:
data = await request.json()
except Exception as exc:
raise HTTPException(400, f"invalid json: {exc}") from exc
if not isinstance(data, dict):
raise HTTPException(400, "json body must be an object")
return data
@app.post("/gh/v1/post_comment")
async def post_comment(request: Request) -> JSONResponse:
data = await _json_body(request)
repo = _require_str(data.get("repo"), "repo")
number = _require_int(data.get("number"), "number")
body = _require_str(data.get("body"), "body")
github: GitHubClient = request.app.state.github
try:
info = await github.post_comment(repo, number, body)
except GitHubError as exc:
return _gh_error_response(exc)
return JSONResponse(_serialize(info))
@app.post("/gh/v1/open_pull_request")
async def open_pull_request(request: Request) -> JSONResponse:
data = await _json_body(request)View on GitHub (pinned to 9690622007)
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
Example fix
// before
await client.post(url, content=JSON.stringify(JSON.stringify(payload)))
// after
await client.post(url, content=JSON.stringify(payload), headers={"content-type": "application/json"}) Defensive patterns
Strategy: validation
Validate before calling
def ensure_json_object(payload):
if not isinstance(payload, dict):
raise ValueError(f"body must be a JSON object, got {type(payload).__name__}")
return payload
ensure_json_object(payload) # call before POSTing Type guard
def is_json_object(v: object) -> TypeGuard[dict]:
return isinstance(v, dict) Try / catch
try:
resp = post_json(url, payload)
except HTTPError as e:
if e.response.status_code == 400 and "json body must be an object" in e.response.text:
# fix payload shape: wrap scalars/arrays into an object before retrying
payload = {"data": payload}
resp = post_json(url, payload)
else:
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Share upload to ${base} failed: server returned no usable id
- Unsupported image type from URL: ${imageUrl}
- Unsupported language '{value}'. Supported: {}
- Unable to infer language from file extension: {}. Specify `l
- Invalid pattern: {err}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/7d62abe093649e31.
Report an issue: GitHub.