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

  1. Inspect the exact request body being sent and ensure the top level is a JSON object: `{"repo": "owner/name", ...}`
  2. Fix double-encoding: call JSON.stringify once on the object, not on an already-serialized string
  3. Ensure the Content-Type header is application/json so the client serializes the object correctly
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/7d62abe093649e31. Report an issue: GitHub.