{"record":{"id":"62ff4eeb0e4bd15f","repo":"can1357/oh-my-pi","slug":"githuberror-resp-status-code-str-msg-retry-afte","errorCode":null,"errorMessage":"GitHubError(resp.status_code, str(msg), retry_after=retry_after)","messagePattern":"GitHubError\\(resp\\.status_code, str\\(msg\\), retry_after=retry_after\\)","errorType":"http","errorClass":"GitHubError","httpStatus":null,"severity":"error","filePath":"python/robomp/src/github_client.py","lineNumber":267,"sourceCode":"\n    def _async_client(self) -> httpx.AsyncClient:\n        return httpx.AsyncClient(\n            base_url=GITHUB_API,\n            headers=self._headers,\n            transport=self._transport,  # type: ignore[arg-type]\n            timeout=httpx.Timeout(30.0, connect=10.0),\n            follow_redirects=True,\n        )\n\n    # ---- request helpers ----\n    def _check(self, resp: httpx.Response) -> Any:\n        if resp.status_code >= 400:\n            retry_after = _parse_retry_after(resp)\n            try:\n                msg = resp.json().get(\"message\", resp.text)\n            except Exception:\n                msg = resp.text\n            raise GitHubError(resp.status_code, str(msg), retry_after=retry_after)\n        if resp.status_code >= 300:\n            # Redirect we couldn't (or weren't asked to) follow. GitHub uses 301\n            # for transferred repos / issues. Surface as a normal error so host\n            # tools map it to RpcCommandError instead of mis-parsing the body.\n            location = resp.headers.get(\"location\", \"\")\n            raise GitHubError(\n                resp.status_code,\n                f\"unexpected redirect to {location!r}; resource may have moved\",\n            )\n        if resp.status_code == 204 or not resp.content:\n            return None\n        return resp.json()\n\n    _TRANSIENT_RETRY_DELAYS = (1.0, 3.0, 10.0)\n    \"\"\"Backoff schedule for transient connection/timeout/5xx errors.\"\"\"\n\n    _TRANSIENT_STATUSES = frozenset({500, 502, 503, 504})\n    \"\"\"Upstream statuses treated as transient — retried for idempotent methods only.\"\"\"","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/github_client.py#L249-L285","documentation":"GitHubError raised by GitHubClient._check whenever the GitHub API responds with status >= 400. It extracts the `message` field from the JSON error body (falling back to raw text) and attaches any `Retry-After` header so callers can rate-limit-aware retry.","triggerScenarios":"Any API call (request_sync, request, _request_text_tail) receiving 4xx/5xx: bad token (401), missing scopes (403), rate limit (403/429 with Retry-After), nonexistent repo/issue (404), validation failure (422), GitHub 5xx.","commonSituations":"Expired GITHUB_TOKEN, hitting the 5000 req/hour REST limit, typos in repo names, forbidden access to private repos, secondary rate limits during bulk operations.","solutions":["Read the embedded status and message to identify the specific cause","If retry_after is set, wait that many seconds before retrying","Verify the token is valid and has required scopes (`gh auth status`, check GH_TOKEN env)","Check rate-limit headers (`x-ratelimit-remaining`) and back off / authenticate requests"],"exampleFix":"// before: unauthenticated burst calls\nfor r in refs: client.request(\"GET\", f\"/repos/{r}\")\n// after: catch and honor retry_after\ntry:\n    data = client.request(\"GET\", path)\nexcept GitHubError as e:\n    if e.retry_after:\n        time.sleep(e.retry_after)\n    else:\n        raise","handlingStrategy":"retry","validationCode":"import os\nif not os.environ.get(\"GITHUB_TOKEN\"):\n    raise RuntimeError(\"GITHUB_TOKEN must be set for API calls\")","typeGuard":"def is_retryable(e: GitHubError) -> bool:\n    return e.retry_after is not None or (500 <= e.status_code < 600)","tryCatchPattern":"try:\n    data = await client.request(\"GET\", path)\nexcept GitHubError as e:\n    if e.retry_after:\n        await asyncio.sleep(e.retry_after)\n        data = await client.request(\"GET\", path)\n    elif 500 <= e.status_code < 600:\n        await asyncio.sleep(backoff)\n        data = await client.request(\"GET\", path)\n    else:\n        raise","preventionTips":["Always authenticate requests to raise the rate limit","Track x-ratelimit-remaining and back off before exhausting","Check `gh auth status` and token scopes when 401/403 appears","Validate repo/issue names client-side to avoid 404s"],"tags":["github","api","http-error"],"backgroundTag":"github-api-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}