{"record":{"id":"7b80a71a8740c7fd","repo":"can1357/oh-my-pi","slug":"comments-idx-start-side-must-be-right-or-left","errorCode":null,"errorMessage":"comments[{idx}].start_side must be RIGHT or LEFT","messagePattern":"comments\\[(.+?)\\]\\.start_side must be RIGHT or LEFT","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"python/robomp/src/proxy/server.py","lineNumber":203,"sourceCode":"    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\n\n\ndef _pool_dir(cfg: Settings, repo: str) -> Path:\n    _validate_repo_name(repo)\n    return Path(cfg.workspace_root) / \"_pool\" / repo.replace(\"/\", \"__\")\n\n\ndef _workspace_repo_dir(cfg: Settings, workspace_key: str) -> Path:\n    # Defense-in-depth: workspace_key is constructed by `sandbox.workspace_key`\n    # as `<repo_with_underscores>__<number>`. Reject anything outside that shape.\n    if \"/\" in workspace_key or workspace_key.startswith(\".\") or \"..\" in workspace_key:\n        raise HTTPException(400, f\"invalid workspace_key {workspace_key!r}\")\n    return Path(cfg.workspace_root) / workspace_key / \"repo\"\n\n","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/proxy/server.py#L185-L221","documentation":"When a review comment spans a range, the optional `start_side` must be a string exactly RIGHT or LEFT. `_require_review_comments` raises this HTTP 400 for the offending index if the provided value differs.","triggerScenarios":"submit_pr_review with a comment carrying start_side values like \"right\", \"SAME\", \"PENDING\", or a non-string coerced through _require_str.","commonSituations":"Multi-line review comments copied from GitHub GraphQL payloads where start_side may be PENDING; clients confusing start_side with start_line; lowercase enum values from internal tooling.","solutions":["Send start_side as exactly \"RIGHT\" or \"LEFT\" (uppercase string), or omit it for single-line comments.","Only include start_side when start_line is also present — ranged comments need both.","Normalize with raw.strip().upper() client-side and validate against {\"RIGHT\",\"LEFT\"} before sending.","Remove PENDING values, which GitHub's API accepts but this proxy does not."],"exampleFix":"// before\n{\"path\": \"a.py\", \"line\": 10, \"start_line\": 5, \"start_side\": \"PENDING\", \"body\": \"x\"}\n// after\n{\"path\": \"a.py\", \"line\": 10, \"start_line\": 5, \"start_side\": \"LEFT\", \"body\": \"x\"}","handlingStrategy":"validation","validationCode":"if raw is not None:\n    s = str(raw).strip().upper()\n    if s not in (\"RIGHT\", \"LEFT\"):\n        raise ValueError(f\"start_side must be RIGHT or LEFT, got {raw!r}\")\n    comment[\"start_side\"] = s","typeGuard":"def is_valid_start_side(v: object) -> TypeGuard[str]:\n    return isinstance(v, str) and v in (\"RIGHT\", \"LEFT\")","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 \"start_side must be\" in e.response.text:\n        raise ValueError(\"start_side must be exactly 'RIGHT' or 'LEFT' (no PENDING)\") from e\n    raise","preventionTips":["Only set start_side together with start_line for ranged comments","Map GraphQL PENDING start_side to the appropriate RIGHT/LEFT before posting","Normalize uppercase/whitespace on enum inputs","Centralize comment building so enums are validated in one place"],"tags":["http-400","input-validation","enum","github-api"],"backgroundTag":"request-parameter-validation","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}