{"record":{"id":"380b42c8ea875f46","repo":"can1357/oh-my-pi","slug":"comments-idx-must-be-an-object","errorCode":null,"errorMessage":"comments[{idx}] must be an object","messagePattern":"comments\\[(.+?)\\] must be an object","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"python/robomp/src/proxy/server.py","lineNumber":188,"sourceCode":"\n\ndef _optional_str_list(value: Any, field: str) -> list[str] | None:\n    if value is None:\n        return None\n    if not isinstance(value, list) or not all(isinstance(v, str) for v in value):\n        raise HTTPException(400, f\"invalid '{field}': must be array of strings\")\n    return list(value)\n\n\ndef _require_review_comments(value: Any) -> list[dict[str, Any]]:\n    if value is None:\n        return []\n    if not isinstance(value, list):\n        raise HTTPException(400, \"missing/invalid 'comments'\")\n    comments: list[dict[str, Any]] = []\n    for idx, item in enumerate(value):\n        if not isinstance(item, dict):\n            raise HTTPException(400, f\"comments[{idx}] must be an object\")\n        path = _require_str(item.get(\"path\"), f\"comments[{idx}].path\")\n        line = _require_int(item.get(\"line\"), f\"comments[{idx}].line\")\n        body = _require_str(item.get(\"body\"), f\"comments[{idx}].body\")\n        side = str(item.get(\"side\") or \"RIGHT\")\n        if side not in (\"RIGHT\", \"LEFT\"):\n            raise HTTPException(400, f\"comments[{idx}].side must be RIGHT or LEFT\")\n        comment: dict[str, Any] = {\"path\": path, \"line\": line, \"side\": side, \"body\": body}\n        start_line = item.get(\"start_line\")\n        if start_line is not None:\n            comment[\"start_line\"] = _require_int(start_line, f\"comments[{idx}].start_line\")\n        start_side = item.get(\"start_side\")\n        if start_side is not None:\n            start_side_str = _require_str(start_side, f\"comments[{idx}].start_side\")\n            if start_side_str not in (\"RIGHT\", \"LEFT\"):\n                raise HTTPException(400, f\"comments[{idx}].start_side must be RIGHT or LEFT\")\n            comment[\"start_side\"] = start_side_str\n        comments.append(comment)\n    return comments","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/proxy/server.py#L170-L206","documentation":"Each element of the `comments` array in a PR review must be an object with path, line, and body. `_require_review_comments` raises this HTTP 400 when an array element is not a JSON object (e.g. a bare string or number).","triggerScenarios":"Posting submit_pr_review with comments like [\"leave a note\"] or [\"path a.py\", 3] instead of objects such as [{\"path\": \"a.py\", \"line\": 3, \"body\": \"note\"}].","commonSituations":"CLI tools accepting free-text review notes and passing them through unstructured; clients submitting an array of file paths expecting the server to attach a default message; template mistakes dropping the object braces.","solutions":["Send each comment as an object with required keys path (string), line (int), and body (string).","Build comments programmatically: comments.append({\"path\": p, \"line\": l, \"body\": b}) rather than appending raw strings.","Validate the payload shape client-side before the request (see defense type guard).","Check for accidental flattening — e.g. list(dict) yields keys only, not objects."],"exampleFix":"// before\n\"comments\": [\"a.py looks wrong\"]\n// after\n\"comments\": [{\"path\": \"a.py\", \"line\": 1, \"body\": \"a.py looks wrong\"}]","handlingStrategy":"validation","validationCode":"def check_comments(comments):\n    for c in comments:\n        if not isinstance(c, dict):\n            raise ValueError(\"each comment must be an object\")\n        for k in (\"path\", \"body\"):\n            if not isinstance(c.get(k), str):\n                raise ValueError(f\"comment.{k} must be a string\")\n        if not isinstance(c.get(\"line\"), int) or isinstance(c[\"line\"], bool):\n            raise ValueError(\"comment.line must be an int\")","typeGuard":"def is_comment(c: object) -> TypeGuard[dict]:\n    return (\n        isinstance(c, dict)\n        and isinstance(c.get(\"path\"), str)\n        and isinstance(c.get(\"line\"), int)\n        and not isinstance(c.get(\"line\"), bool)\n        and isinstance(c.get(\"body\"), str)\n    )","tryCatchPattern":"try:\n    resp = http.post(f\"{base}/pr/{n}/review\", json=payload)\n    resp.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and \"must be an object\" in e.response.text:\n        idx = int(e.response.text.split(\"comments[\")[1].split(\"]\")[0])\n        raise ValueError(f\"comments[{idx}] is not an object: {payload['comments'][idx]!r}\") from e\n    raise","preventionTips":["Build each comment as a dict with path/line/body keys, never raw strings","Add a local payload validator mirroring the server's rules","Don't pass list(dict) or other accidental key-flattening results as comments","Unit-test payload builders so a shape regression fails before the HTTP call"],"tags":["http-400","input-validation","fastapi","github-api"],"backgroundTag":"request-parameter-validation","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}