can1357/oh-my-pi · error · GitHubError

GitHubError(resp.status_code, str(msg), retry_after=retry_af

Error message

GitHubError(resp.status_code, str(msg), retry_after=retry_after)

What it means

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.

Source

Thrown at python/robomp/src/github_client.py:267

    def _async_client(self) -> httpx.AsyncClient:
        return httpx.AsyncClient(
            base_url=GITHUB_API,
            headers=self._headers,
            transport=self._transport,  # type: ignore[arg-type]
            timeout=httpx.Timeout(30.0, connect=10.0),
            follow_redirects=True,
        )

    # ---- request helpers ----
    def _check(self, resp: httpx.Response) -> Any:
        if resp.status_code >= 400:
            retry_after = _parse_retry_after(resp)
            try:
                msg = resp.json().get("message", resp.text)
            except Exception:
                msg = resp.text
            raise GitHubError(resp.status_code, str(msg), retry_after=retry_after)
        if resp.status_code >= 300:
            # Redirect we couldn't (or weren't asked to) follow. GitHub uses 301
            # for transferred repos / issues. Surface as a normal error so host
            # tools map it to RpcCommandError instead of mis-parsing the body.
            location = resp.headers.get("location", "")
            raise GitHubError(
                resp.status_code,
                f"unexpected redirect to {location!r}; resource may have moved",
            )
        if resp.status_code == 204 or not resp.content:
            return None
        return resp.json()

    _TRANSIENT_RETRY_DELAYS = (1.0, 3.0, 10.0)
    """Backoff schedule for transient connection/timeout/5xx errors."""

    _TRANSIENT_STATUSES = frozenset({500, 502, 503, 504})
    """Upstream statuses treated as transient — retried for idempotent methods only."""

View on GitHub (pinned to 9690622007)

Solutions

  1. Read the embedded status and message to identify the specific cause
  2. If retry_after is set, wait that many seconds before retrying
  3. Verify the token is valid and has required scopes (`gh auth status`, check GH_TOKEN env)
  4. Check rate-limit headers (`x-ratelimit-remaining`) and back off / authenticate requests

Example fix

// before: unauthenticated burst calls
for r in refs: client.request("GET", f"/repos/{r}")
// after: catch and honor retry_after
try:
    data = client.request("GET", path)
except GitHubError as e:
    if e.retry_after:
        time.sleep(e.retry_after)
    else:
        raise
Defensive patterns

Strategy: retry

Validate before calling

import os
if not os.environ.get("GITHUB_TOKEN"):
    raise RuntimeError("GITHUB_TOKEN must be set for API calls")

Type guard

def is_retryable(e: GitHubError) -> bool:
    return e.retry_after is not None or (500 <= e.status_code < 600)

Try / catch

try:
    data = await client.request("GET", path)
except GitHubError as e:
    if e.retry_after:
        await asyncio.sleep(e.retry_after)
        data = await client.request("GET", path)
    elif 500 <= e.status_code < 600:
        await asyncio.sleep(backoff)
        data = await client.request("GET", path)
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/62ff4eeb0e4bd15f. Report an issue: GitHub.