{"record":{"id":"d302b2ad109ca46d","repo":"zed-industries/zed","slug":"rest-get-path-failed-after-retries-retries-l","errorCode":null,"errorMessage":"REST GET {path} failed after {retries} retries: {last_err}","messagePattern":"REST GET (.+?) failed after (.+?) retries: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"script/triage_project_sync.py","lineNumber":178,"sourceCode":"    last_err: Exception | None = None\n    for attempt in range(retries):\n        try:\n            r = requests.get(url, headers=headers_rest(), params=params, timeout=30)\n            if r.status_code == 200:\n                return r.json()\n            if r.status_code in (429, 502, 503, 504):\n                wait = 2**attempt * 2\n                log(f\"REST {r.status_code} on {path}; retry in {wait}s\", \"WARN\")\n                time.sleep(wait)\n                continue\n            log(f\"REST GET {path} failed: {r.status_code} {r.text[:200]}\", \"ERROR\")\n            r.raise_for_status()\n        except requests.RequestException as e:\n            last_err = e\n            wait = 2**attempt * 2\n            log(f\"REST GET {path} threw {e}; retry in {wait}s\", \"WARN\")\n            time.sleep(wait)\n    raise RuntimeError(f\"REST GET {path} failed after {retries} retries: {last_err}\")\n\n\ndef rest_get_paginated(path: str, params: dict | None = None, max_pages: int = 20) -> list:\n    p = dict(params or {})\n    p[\"per_page\"] = 100\n    out: list = []\n    for page in range(1, max_pages + 1):\n        p[\"page\"] = page\n        items = rest_get(path, p)\n        if not items:\n            break\n        if not isinstance(items, list):\n            log(f\"REST {path} page {page} returned non-list\", \"WARN\")\n            break\n        out.extend(items)\n        if len(items) < 100:\n            break\n    return out","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/zed-industries/zed/blob/bc538def4545534201bbfcac4e95ac34ea6501b6/script/triage_project_sync.py#L160-L196","documentation":"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'.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nif r.status_code in (429, 502, 503, 504):\n    wait = 2**attempt * 2\n    log(f\"REST {r.status_code} on {path}; retry in {wait}s\", \"WARN\")\n    time.sleep(wait)\n    continue\nlog(f\"REST GET {path} failed: {r.status_code} {r.text[:200]}\", \"ERROR\")\nr.raise_for_status()\n\n// after\nif r.status_code in (429, 502, 503, 504) and attempt < retries - 1:\n    last_err = RuntimeError(f\"HTTP {r.status_code}\")\n    wait = 2**attempt * 2\n    log(f\"REST {r.status_code} on {path}; retry in {wait}s\", \"WARN\")\n    time.sleep(wait)\n    continue\nif r.status_code != 200:\n    raise RuntimeError(f\"REST GET {path}: HTTP {r.status_code} {r.text[:200]}\")\nreturn r.json()","handlingStrategy":"retry","validationCode":"def rest_path_is_well_formed(path: str) -> bool:\n    return bool(path) and not path.startswith(\"/\") and \" \" not in path","typeGuard":"def is_transient_http_status(status: int) -> bool:\n    return status in (429, 502, 503, 504)","tryCatchPattern":"try:\n    items = rest_get(path, params)\nexcept RuntimeError as exc:\n    if \"failed after\" in str(exc):\n        raise SystemExit(f\"Persistent GitHub REST failure on {path}; see logs above for the final status\")\n    raise","preventionTips":["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"],"tags":["github-api","rest","retry","rate-limit","python"],"backgroundTag":null,"analyzedSha":"bc538def4545534201bbfcac4e95ac34ea6501b6","analyzedAt":"2026-08-16T07:30:46.435Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}