{"record":{"id":"23517543663f5ec0","repo":"zed-industries/zed","slug":"unexpected-response-for-issue-number","errorCode":null,"errorMessage":"unexpected response for issue {number}","messagePattern":"unexpected response for issue (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"script/triage_project_sync.py","lineNumber":259,"sourceCode":"    created_at: datetime\n    reporter: str\n    assignees: list[str]\n    labels: list[str]\n    issue_type: str | None  # e.g. \"Bug\", \"Crash\", \"Meta\", \"Tracking\", or None\n    is_pull_request: bool\n    comments: list[dict]\n\n\ndef parse_dt(s: str | None) -> datetime | None:\n    if not s:\n        return None\n    return datetime.fromisoformat(s.replace(\"Z\", \"+00:00\"))\n\n\ndef fetch_issue(number: int) -> IssueData:\n    issue = rest_get(f\"repos/{REPO}/issues/{number}\")\n    if not isinstance(issue, dict):\n        raise RuntimeError(f\"unexpected response for issue {number}\")\n    comments = rest_get_paginated(f\"repos/{REPO}/issues/{number}/comments\")\n    created_at = parse_dt(issue[\"created_at\"])\n    if created_at is None:\n        raise RuntimeError(f\"issue {number} has no created_at\")\n    issue_type = None\n    if isinstance(issue.get(\"type\"), dict):\n        issue_type = issue[\"type\"].get(\"name\")\n    return IssueData(\n        number=number,\n        node_id=issue[\"node_id\"],\n        title=issue[\"title\"],\n        state=issue[\"state\"],\n        closed_at=parse_dt(issue.get(\"closed_at\")),\n        created_at=created_at,\n        reporter=issue[\"user\"][\"login\"],\n        assignees=[a[\"login\"] for a in (issue.get(\"assignees\") or [])],\n        labels=[l[\"name\"] for l in issue[\"labels\"]],\n        issue_type=issue_type,","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/zed-industries/zed/blob/bc538def4545534201bbfcac4e95ac34ea6501b6/script/triage_project_sync.py#L241-L277","documentation":"Raised by fetch_issue() in the triage project sync script when the GitHub REST call for a single issue returns HTTP 200 but the parsed JSON body is not an object. rest_get() already retries 429/5xx and raises for other statuses, so reaching this check means GitHub (or something on the wire) answered 200 with a payload whose shape is not an issue object.","triggerScenarios":"Calling rest_get(f\"repos/{REPO}/issues/{number}\") with a falsy/empty number (URL becomes repos/{REPO}/issues/ which lists issues and returns a JSON array); REPO malformed so the path resolves to a list-returning endpoint; a transparent proxy or captive portal injecting a 200 JSON response that is not the issue object.","commonSituations":"REPO env var not in 'owner/repo' form; issue number passed as None or '' from an upstream parsing bug; corporate proxy mangling api.github.com responses; GitHub returning an unexpected-but-200 body during an incident.","solutions":["Log the actual payload and type for the failing number (include {issue!r} in the message) to see what came back","Check how number was produced upstream — an empty value turns the single-issue endpoint into the list endpoint","Verify REPO is exactly 'owner/repo' and the token is valid so the response is a real issue object","If a proxy is in play, curl the same URL with the same headers from that environment"],"exampleFix":"# before\nissue = rest_get(f\"repos/{REPO}/issues/{number}\")\nif not isinstance(issue, dict):\n    raise RuntimeError(f\"unexpected response for issue {number}\")\n\n# after\nissue = rest_get(f\"repos/{REPO}/issues/{number}\")\nif not isinstance(issue, dict):\n    raise RuntimeError(\n        f\"unexpected response for issue {number}: \"\n        f\"{type(issue).__name__} {str(issue)[:200]}\"\n    )","handlingStrategy":"validation","validationCode":"# Before the sync run, fail fast on bad inputs and auth\nimport os, requests\n\ndef validate_sync_inputs(repo: str, numbers: list[int]) -> None:\n    if \"/\" not in repo or repo.count(\"/\") != 1:\n        raise SystemExit(f\"REPO must be 'owner/repo', got {repo!r}\")\n    bad = [n for n in numbers if not isinstance(n, int) or n <= 0]\n    if bad:\n        raise SystemExit(f\"invalid issue numbers (must be positive ints): {bad}\")\n    token = os.environ.get(\"GITHUB_TOKEN\")\n    if token:\n        r = requests.get(\"https://api.github.com/user\",\n                         headers={\"Authorization\": f\"Bearer {token}\"}, timeout=10)\n        r.raise_for_status()","typeGuard":"def is_issue_payload(payload: object) -> bool:\n    \"\"\"A single-issue REST response is an object containing 'number'.\"\"\"\n    return isinstance(payload, dict) and \"number\" in payload and \"created_at\" in payload","tryCatchPattern":"try:\n    data = fetch_issue(number)\nexcept RuntimeError as error:\n    log(f\"skipping issue {number}: {error}\", \"WARN\")\n    continue  # one malformed issue must not abort the whole sync","preventionTips":["Validate REPO format and issue numbers before the loop, not per-request","Keep the isinstance check but include a payload excerpt in the error for diagnosability","Pre-flight the token with a /user request so auth failures surface as 401, not shape surprises"],"tags":["python","github-api","triage","rest","validation"],"backgroundTag":null,"analyzedSha":"bc538def4545534201bbfcac4e95ac34ea6501b6","analyzedAt":"2026-08-16T07:30:46.435Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}