{"record":{"id":"eea4422357b54e16","repo":"google-gemini/gemini-cli","slug":"failed-to-fetch-issue-issue-number-from-github","errorCode":null,"errorMessage":"Failed to fetch issue #{issue_number} from GitHub API ({resp.status_code}): {resp.text}","messagePattern":"Failed to fetch issue #(.+?) from GitHub API \\((.+?)\\): (.+?)","errorType":"http","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"tools/caretaker-agent/evals/triage/helpers/github_api.py","lineNumber":29,"sourceCode":"\n\ndef _get_github_headers() -> Dict[str, str]:\n    \"\"\"\n    Optionally retrieves GITHUB_TOKEN (or GH_TOKEN) to authenticate requests.\n    \"\"\"\n    token = os.environ.get(\"GITHUB_TOKEN\") or os.environ.get(\"GH_TOKEN\")\n    headers = {\"Accept\": \"application/vnd.github.v3+json\"}\n    if token:\n        headers[\"Authorization\"] = f\"Bearer {token}\"\n    return headers\n\n\ndef get_issue_details(owner: str, repo: str, issue_number: int) -> Dict[str, Any]:\n    \"\"\"Queries GitHub REST API for issue details (title, body, createdAt, labels).\"\"\"\n    url = f\"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}\"\n    resp = requests.get(url, headers=_get_github_headers(), timeout=15)\n    if resp.status_code != 200:\n        raise RuntimeError(f\"Failed to fetch issue #{issue_number} from GitHub API ({resp.status_code}): {resp.text}\")\n    \n    data = resp.json()\n    return {\n        \"owner\": owner,\n        \"repo\": repo,\n        \"number\": data.get(\"number\"),\n        \"title\": data.get(\"title\", \"\"),\n        \"body\": data.get(\"body\", \"\") or \"\",\n        \"createdAt\": data.get(\"created_at\", \"\"),\n        \"labels\": data.get(\"labels\", [])\n    }\n\n\ndef get_pr_details(owner: str, repo: str, pr_number: int) -> Dict[str, Any]:\n    \"\"\"Queries GitHub REST API for PR details (title, body, baseRefOid, patch/diff).\"\"\"\n    url = f\"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}\"\n    headers = _get_github_headers()\n    resp = requests.get(url, headers=headers, timeout=15)","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/5024443c7217464a66e98f80d73172a26440bd8f/tools/caretaker-agent/evals/triage/helpers/github_api.py#L11-L47","documentation":"This RuntimeError is raised by get_issue_details() when the GitHub REST API GET to /repos/{owner}/{repo}/issues/{issue_number} returns any non-200 HTTP status. The error includes the status code and raw response body so the caller can distinguish rate limits (403/429), auth failures (401), not-found (404), and server errors (5xx). It is a hard failure because the eval pipeline cannot proceed without issue data.","triggerScenarios":"Calling get_issue_details(owner, repo, issue_number) with a wrong owner/repo, an issue number that does not exist (404), no token when the repo is private (404/403), an expired/invalid GITHUB_TOKEN (401), or hitting a secondary rate limit (403 with 'rate limit' message, or 429). Network issues typically raise ConnectionError before this guard.","commonSituations":"GITHUB_TOKEN/GH_TOKEN is unset or expired and the eval tries to fetch a private-repo issue. A typo in owner or repo. The issue was deleted or is actually a PR (use the pulls endpoint). GitHub primary rate limit exhausted (5000 req/hr authenticated). Cloud Run egress blocked or DNS failing producing a non-200 from a proxy.","solutions":["Set a valid GITHUB_TOKEN (or GH_TOKEN) env var with repo read scope and retry.","Verify owner, repo, and issue_number are correct and that the issue exists at the URL in a browser.","If status is 403/429, wait for the rate-limit window to reset (check X-RateLimit-Reset header) or reduce concurrency in the eval runner.","Add a retry with exponential backoff around get_issue_details for transient 5xx responses.","Confirm Cloud Run / network egress to api.github.com is permitted."],"exampleFix":"# before\nos.environ.pop('GITHUB_TOKEN', None)  # anonymous, hits rate limit fast\n# after\nos.environ['GITHUB_TOKEN'] = 'ghp_...'  # authenticated, 5000 req/hr","handlingStrategy":"retry","validationCode":"import os, requests\n\ndef can_fetch_issue(owner: str, repo: str, issue_number: int) -> bool:\n    headers = {'Accept': 'application/vnd.github.v3+json'}\n    token = os.environ.get('GITHUB_TOKEN') or os.environ.get('GH_TOKEN')\n    if token:\n        headers['Authorization'] = f'Bearer {token}'\n    resp = requests.get(f'https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}', headers=headers, timeout=15)\n    return resp.status_code == 200","typeGuard":null,"tryCatchPattern":"import time\nfrom evals.triage.helpers.github_api import get_issue_details\n\nfor attempt in range(3):\n    try:\n        issue = get_issue_details(owner, repo, issue_number)\n        break\n    except RuntimeError as e:\n        if '403' in str(e) or '429' in str(e):\n            time.sleep(2 ** attempt)\n        else:\n            raise\nelse:\n    raise RuntimeError(f'Could not fetch issue {issue_number} after retries')","preventionTips":["Set a valid GITHUB_TOKEN/GH_TOKEN with appropriate scope before running the eval.","Respect X-RateLimit-Remaining and back off near the limit.","Validate owner/repo/issue_number against the live API in a pre-flight check.","Cache issue JSON locally to avoid refetching across eval runs."],"tags":["github-api","network","rate-limit","auth","evals","python"],"backgroundTag":null,"analyzedSha":"5024443c7217464a66e98f80d73172a26440bd8f","analyzedAt":"2026-08-12T06:01:53.711Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}