zed-industries/zed · error · RuntimeError

REST GET {path} failed after {retries} retries: {last_err}

Error message

REST GET {path} failed after {retries} retries: {last_err}

What it means

rest_get gives up after `retries` (3) attempts and raises, naming the path and the last exception. Note two behaviors visible in the source: every non-200 that is not 429/502/503/504 goes through raise_for_status(), whose HTTPError is then caught and retried like a network error — so 401/403/404 are retried pointlessly before this raise; and when all attempts took the transient-status `continue` branch, last_err stays None and the message ends with 'None'.

Source

Thrown at script/triage_project_sync.py:178

    last_err: Exception | None = None
    for attempt in range(retries):
        try:
            r = requests.get(url, headers=headers_rest(), params=params, timeout=30)
            if r.status_code == 200:
                return r.json()
            if r.status_code in (429, 502, 503, 504):
                wait = 2**attempt * 2
                log(f"REST {r.status_code} on {path}; retry in {wait}s", "WARN")
                time.sleep(wait)
                continue
            log(f"REST GET {path} failed: {r.status_code} {r.text[:200]}", "ERROR")
            r.raise_for_status()
        except requests.RequestException as e:
            last_err = e
            wait = 2**attempt * 2
            log(f"REST GET {path} threw {e}; retry in {wait}s", "WARN")
            time.sleep(wait)
    raise RuntimeError(f"REST GET {path} failed after {retries} retries: {last_err}")


def rest_get_paginated(path: str, params: dict | None = None, max_pages: int = 20) -> list:
    p = dict(params or {})
    p["per_page"] = 100
    out: list = []
    for page in range(1, max_pages + 1):
        p["page"] = page
        items = rest_get(path, p)
        if not items:
            break
        if not isinstance(items, list):
            log(f"REST {path} page {page} returned non-list", "WARN")
            break
        out.extend(items)
        if len(items) < 100:
            break
    return out

View on GitHub (pinned to bc538def45)

Solutions

  1. Read the WARN/ERROR log lines just above the raise — they carry the per-attempt status code and body snippet
  2. Fix the underlying cause: rotate the token (401/403), correct the path (404), lengthen backoff on 429
  3. Stop retrying non-transient 4xx: fail immediately on 401/403/404 to surface the real problem
  4. Set last_err in the transient-status branch too, so the final message never reports None

Example fix

// before
if r.status_code in (429, 502, 503, 504):
    wait = 2**attempt * 2
    log(f"REST {r.status_code} on {path}; retry in {wait}s", "WARN")
    time.sleep(wait)
    continue
log(f"REST GET {path} failed: {r.status_code} {r.text[:200]}", "ERROR")
r.raise_for_status()

// after
if r.status_code in (429, 502, 503, 504) and attempt < retries - 1:
    last_err = RuntimeError(f"HTTP {r.status_code}")
    wait = 2**attempt * 2
    log(f"REST {r.status_code} on {path}; retry in {wait}s", "WARN")
    time.sleep(wait)
    continue
if r.status_code != 200:
    raise RuntimeError(f"REST GET {path}: HTTP {r.status_code} {r.text[:200]}")
return r.json()
Defensive patterns

Strategy: retry

Validate before calling

def rest_path_is_well_formed(path: str) -> bool:
    return bool(path) and not path.startswith("/") and " " not in path

Type guard

def is_transient_http_status(status: int) -> bool:
    return status in (429, 502, 503, 504)

Try / catch

try:
    items = rest_get(path, params)
except RuntimeError as exc:
    if "failed after" in str(exc):
        raise SystemExit(f"Persistent GitHub REST failure on {path}; see logs above for the final status")
    raise

Prevention

When it happens

Trigger: Persistent 502/503/504 or 429 across all three attempts; persistent 401/403/404 (expired token, bad path, missing resource) retried anyway; per-attempt ConnectionError/Timeout (DNS, egress blocked).

Common situations: Expired GITHUB_TOKEN yielding 401 three times; secondary rate limits during overlapping CI runs; typo'd API path returning 404 each time; runner network restrictions causing repeated connection failures.

Related errors


AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16). Data as JSON: /api/errors/d302b2ad109ca46d. Report an issue: GitHub.