{"record":{"id":"76a1179cf957261a","repo":"can1357/oh-my-pi","slug":"missing-invalid-comments","errorCode":null,"errorMessage":"missing/invalid 'comments'","messagePattern":"missing/invalid 'comments'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"python/robomp/src/proxy/server.py","lineNumber":184,"sourceCode":"        return None\n    if not isinstance(value, int) or isinstance(value, bool) or not (0 < value < 65536):\n        raise HTTPException(400, \"missing/invalid 'slot_uid'\")\n    return value\n\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\"):","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/proxy/server.py#L166-L202","documentation":"`_require_review_comments` validates the `comments` array of a PR review submission. `comments` must be a JSON array (null is tolerated and becomes []); a non-array value raises this HTTP 400. Individual element validation produces the more specific comments[idx] errors instead.","triggerScenarios":"Calling submit_pr_review with `comments` as an object, a string, or a number — e.g. sending a single comment object directly instead of wrapping it in a list, or sending comments as a JSON-encoded string.","commonSituations":"Clients double-encoding comments (JSON string inside JSON); SDK misuse where a single-comment convenience path forgets the array wrapper; copying a comments object from a GET response and posting it back unmodified.","solutions":["Wrap the comment payload in an array: [{\"path\": ..., \"line\": ..., \"body\": ...}].","If the value is a JSON string, parse it client-side before posting.","Omit `comments` entirely (or send null) for review verdicts without comments — null is accepted.","Inspect the request body with a debugger/logging middleware to confirm the actual encoded type."],"exampleFix":"// before\n{\"event\": \"COMMENT\", \"comments\": {\"path\": \"a.py\", \"line\": 3, \"body\": \"fix\"}}\n// after\n{\"event\": \"COMMENT\", \"comments\": [{\"path\": \"a.py\", \"line\": 3, \"body\": \"fix\"}]}","handlingStrategy":"type-guard","validationCode":"comments = raw if isinstance(raw, list) else ([raw] if isinstance(raw, dict) else None)\nif comments is None:\n    raise ValueError(\"comments must be a list of comment objects\")","typeGuard":"def is_comment_list(v: object) -> TypeGuard[list[dict]]:\n    return isinstance(v, list) and all(isinstance(x, dict) for x in v)","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 \"'comments'\" in e.response.text:\n        raise ValueError(\"comments must be a JSON array, not \" + type(payload['comments']).__name__) from e\n    raise","preventionTips":["Never JSON-encode comments twice; post the parsed structure","Wrap single comment objects in a list at the call site","Model the review payload with pydantic so arrays are enforced locally","When re-posting data from a GET response, rebuild the payload explicitly"],"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"}