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 outView on GitHub (pinned to bc538def45)
Solutions
- Read the WARN/ERROR log lines just above the raise — they carry the per-attempt status code and body snippet
- Fix the underlying cause: rotate the token (401/403), correct the path (404), lengthen backoff on 429
- Stop retrying non-transient 4xx: fail immediately on 401/403/404 to surface the real problem
- 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
- Log the final status code in the exception, not just per-attempt logs
- Do not retry non-transient 4xx responses
- Set last_err on the transient-status branch so messages never show None
- Budget rate limit usage before long pagination loops
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
- GraphQL failed after {retries} retries: {last_err}
- GraphQL errors: {json.dumps(data['errors'])[:300]}
- GraphQL error: {result['errors']}
- GraphQL error: {result['errors']}
- No assignment event for {assignee} on issue #{issue_number}
AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16).
Data as JSON: /api/errors/d302b2ad109ca46d.
Report an issue: GitHub.