{"record":{"id":"0874719833cf73c7","repo":"can1357/oh-my-pi","slug":"github-error-exc-status-exc-message","errorCode":null,"errorMessage":"github error: {exc.status} {exc.message}","messagePattern":"github error: (.+?) (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":502,"severity":"error","filePath":"python/robomp/src/server.py","lineNumber":632,"sourceCode":"            try:\n                repo_full, number = parse_issue_ref(issue_ref)\n            except InvalidIssueRef as exc:\n                raise HTTPException(400, str(exc)) from exc\n            if not cfg.allows(repo_full):\n                raise HTTPException(403, f\"{repo_full} not in ROBOMP_REPO_ALLOWLIST\")\n            try:\n                delivery = await enqueue_manual_triage(\n                    db=db,\n                    github=github,\n                    repo_full=repo_full,\n                    number=number,\n                )\n            except ManualTriageConflict as exc:\n                raise HTTPException(409, str(exc)) from exc\n            except ManualTriageError as exc:\n                raise HTTPException(400, str(exc)) from exc\n            except GitHubError as exc:\n                raise HTTPException(502, f\"github error: {exc.status} {exc.message}\") from exc\n            pool.wake()\n            log.info(\"manual triage\", extra={\"delivery\": delivery, \"issue\": f\"{repo_full}#{number}\"})\n            return JSONResponse(\n                {\"delivery\": delivery, \"state\": \"queued\", \"mode\": \"triage\"},\n                status_code=202,\n            )\n\n        # mode == \"retry\"\n        if isinstance(delivery_id, str) and delivery_id:\n            target = delivery_id\n        elif isinstance(issue_ref, str) and issue_ref:\n            try:\n                repo_full, number = parse_issue_ref(issue_ref)\n            except InvalidIssueRef as exc:\n                raise HTTPException(400, str(exc)) from exc\n            if not cfg.allows(repo_full):\n                raise HTTPException(403, f\"{repo_full} not in ROBOMP_REPO_ALLOWLIST\")\n            row = db.latest_event_for_issue(make_issue_key(repo_full, number))","sourceCodeStart":614,"sourceCodeEnd":650,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/server.py#L614-L650","documentation":"A GitHubError escaped the triage call and api_trigger converts it into HTTP 502 with the message 'github error: {status} {message}'. The server successfully received your request but its call to the GitHub API failed with HTTP status `exc.status` and message `exc.message`. This marks an upstream dependency failure rather than a problem with your request.","triggerScenarios":"Any trigger request where the triage path performs GitHub API calls (fetching the issue, labels, permissions, or posting triage results) and GitHub responds with a non-OK status — 401/403 for bad or expired tokens, 404 for a repo the token cannot see, 422 for invalid API input, 403 with rate-limit headers when the token is exhausted.","commonSituations":"ROBOMP GitHub token expired or rotated without updating the server config; token lacking access to a private repo in the allowlist; hitting GitHub's rate limit during webhook storms; a branch/issue being deleted between triage queuing and execution; GitHub incidents/5xx outages.","solutions":["Read `exc.status` and `exc.message` from the 502 body — they mirror GitHub's own response and pinpoint the cause","If 401/403: verify and refresh the GitHub token used by the server and confirm it has access to the target repo","If 403 with rate-limit message: wait for the rate-limit window to reset or use a token with a higher quota","If 404: confirm the repo exists and the token can see it (private repo membership)","If 5xx: check GitHub status and retry the trigger once the upstream recovers"],"exampleFix":"// before: token from stale env\nclient = GitHub(token=os.environ['OLD_GH_TOKEN'])\n// after: refreshed token with required scopes\nclient = GitHub(token=os.environ['GH_TOKEN'])  # updated secret, repo scope granted","handlingStrategy":"retry","validationCode":"// Pre-flight: verify the token can read the target repo before triggering triage\nconst check = await fetch(`https://api.github.com/repos/${repoFull}`, {\n  headers: { Authorization: `Bearer ${ghToken}`, Accept: 'application/vnd.github+json' },\n});\nif (!check.ok) throw new Error(`GitHub pre-flight failed: ${check.status} ${await check.text()}`);","typeGuard":"function isGitHubErrorDetail(body: unknown): body is { detail: string } & { status: number } {\n  return typeof body === 'object' && body !== null &&\n    typeof (body as any).detail === 'string' && /^github error: \\d{3} /.test((body as any).detail);\n}","tryCatchPattern":"try {\n  await api.trigger(payload);\n} catch (err) {\n  const m = /^github error: (\\d{3}) /.exec(err.message ?? '');\n  if (m) {\n    const gh = Number(m[1]);\n    if (gh === 403 && /rate limit/i.test(err.message)) await Bun.sleep(rateLimitResetMs);\n    else if (gh >= 500) await Bun.sleep(retryMs); // transient GitHub outage\n    else logger.error('GitHub rejected the server call', { gh, detail: err.message }); // 401/403/404: fix token/repo\n  } else throw err;\n}","preventionTips":["Rotate and verify the server's GitHub token before it expires","Grant the token access to every repo in ROBOMP_REPO_ALLOWLIST, including private ones","Monitor GitHub rate-limit headers and throttle webhook-triggered triage accordingly","Subscribe to GitHub status updates; suppress auto-retries during incidents"],"tags":["http-502","github","upstream","api-error","rate-limit"],"backgroundTag":"upstream-api-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}