{"record":{"id":"673759a93975e91d","repo":"chatboxai/chatbox","slug":"github-api-error-response-status-response-st","errorCode":null,"errorMessage":"GitHub API error: ${response.status} ${response.statusText}","messagePattern":"GitHub API error: (.+?) (.+?)","errorType":"exception","errorClass":"GitHubApiError","httpStatus":null,"severity":"error","filePath":"src/main/skills/github-fetcher.ts","lineNumber":92,"sourceCode":"async function githubFetch<T>(url: string): Promise<T> {\n  const cached = getCached<T>(url)\n  if (cached !== undefined) return cached\n\n  const response = await fetch(url, {\n    headers: {\n      'User-Agent': USER_AGENT,\n      Accept: 'application/vnd.github.v3+json',\n    },\n  })\n\n  if (!response.ok) {\n    if (response.status === 404) {\n      throw new GitHubApiError(`Not found: ${url}`, 404)\n    }\n    if (response.status === 403) {\n      throw new GitHubApiError('GitHub API rate limit exceeded. Try again later.', 403)\n    }\n    throw new GitHubApiError(`GitHub API error: ${response.status} ${response.statusText}`, response.status)\n  }\n\n  const data = (await response.json()) as T\n  setCache(url, data)\n  return data\n}\n\nasync function fetchRepoTree(owner: string, repo: string): Promise<GitHubTreeResponse | null> {\n  const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`\n  const result = await githubFetch<GitHubTreeResponse>(url)\n  if (!Array.isArray(result?.tree)) {\n    log.warn(`Unexpected tree response shape for ${owner}/${repo}`)\n    return null\n  }\n  if (result.truncated) {\n    log.warn(`Tree listing truncated for ${owner}/${repo}`)\n    return null\n  }","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/skills/github-fetcher.ts#L74-L110","documentation":"Thrown by githubFetch() as a generic catch-all when the GitHub REST API (api.github.com) returns a non-OK status that is not 404 or 403. The error carries the HTTP status code and statusText so callers can distinguish transient 5xx failures from auth/validation problems (401, 422, 451). githubFetch is the shared gateway for all GitHub data access in the skills module (repo trees, contents, raw file fetches) and is called without authentication, so it is subject to the 60 req/hr unauthenticated rate limit.","triggerScenarios":"Calling skills:scan-repo or skills:install with an owner/repo whose API responds with 401 (repo made private), 422 (validation), 451 (DMCA-taken-down), 500/502/503 (GitHub outage), or 5xx from raw.githubusercontent. Also reached when a transient network proxy returns a non-standard status. Any githubFetch-based call (fetchRepoTree, fetchRepoContents, detectSkillsInRepo) hits this path after 404/403 are filtered out.","commonSituations":"Repo was renamed/deleted/made private after a skill was installed; GitHub is having an incident (5xx); corporate proxy injects a 407/502; the owner/repo pair is mistyped and not in a shape that yields 404 (e.g. empty owner triggers 404, but an org with a forbidden action yields 403/401). Unauthenticated calls during heavy skill discovery exhaust the 60/hr limit and surface as 403 first, but secondary calls during the same window can land here.","solutions":["Verify the owner/repo slug is correct and that the repository is public (or add a token-backed fetch path).","Check https://www.githubstatus.com for an active GitHub API incident if the status starts with 5.","Wait for the rate-limit window to reset and clear the in-memory cache via clearCache() before retrying, since successful responses are cached for 5 minutes.","If recurring, switch to an authenticated request path so the limit is 5000/hr instead of 60/hr."],"exampleFix":"// before\nthrow new GitHubApiError(`GitHub API error: ${response.status} ${response.statusText}`, response.status)\n\n// after — distinguish retryable 5xx from permanent 4xx so callers can back off\nconst isRetryable = response.status >= 500\nthrow new GitHubApiError(\n  `GitHub API error: ${response.status} ${response.statusText}`,\n  response.status,\n  isRetryable\n)","handlingStrategy":"retry","validationCode":"// Before scanning, sanity-check the slug and that the user has rate-limit budget.\nfunction isValidRepoSlug(owner: string, repo: string): boolean {\n  return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(owner) && /^[A-Za-z0-9_.-]{1,100}$/.test(repo)\n}\nif (!isValidRepoSlug(owner, repo)) throw new Error('Invalid owner/repo slug')","typeGuard":"import { GitHubApiError } from './github-fetcher'\nfunction isGitHubApiError(e: unknown): e is GitHubApiError {\n  return e instanceof Error && (e as GitHubApiError).name === 'GitHubApiError' && typeof (e as GitHubApiError).statusCode === 'number'\n}\nfunction isRetryableStatus(code: number): boolean {\n  return code === 429 || code >= 500\n}","tryCatchPattern":"try {\n  await detectSkillsInRepo(owner, repo)\n} catch (error) {\n  if (isGitHubApiError(error) && isRetryableStatus(error.statusCode)) {\n    clearCache()\n    await backoffRetry(() => detectSkillsInRepo(owner, repo))\n  } else if (isGitHubApiError(error) && error.statusCode === 404) {\n    showUser('Repository not found')\n  } else {\n    throw error\n  }\n}","preventionTips":["Clear the githubFetch cache before retrying so stale successful responses do not mask the failure.","Use an authenticated token for GitHub API calls to lift the 60/hr unauthenticated limit to 5000/hr.","Validate the owner/repo slug format before issuing any API call to avoid wasted requests."],"tags":["github-api","network","rate-limit","skills","http"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}