{"record":{"id":"f9ce6519d91a92fd","repo":"chatboxai/chatbox","slug":"github-api-rate-limit-exceeded-try-again-later","errorCode":null,"errorMessage":"GitHub API rate limit exceeded. Try again later.","messagePattern":"GitHub API rate limit exceeded\\. Try again later\\.","errorType":"exception","errorClass":"GitHubApiError","httpStatus":403,"severity":"warning","filePath":"src/main/skills/github-fetcher.ts","lineNumber":90,"sourceCode":"}\n\nasync 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}`)","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/skills/github-fetcher.ts#L72-L108","documentation":"GitHubApiError with status 403, thrown by githubFetch when GitHub responds 403. The code attributes all 403s to rate-limiting, which is the dominant cause for unauthenticated GitHub API calls (60 req/hour/IP) but note 403 can also mean a forbidden private repo or an abuse-detection block.","triggerScenarios":"Unauthenticated requests to api.github.com exceeding 60/hour per IP; a burst of skill-sync fetches (repo tree + per-file content) blowing the budget; secondary rate limits tripped by concurrent requests; legitimate 403 on a forbidden resource misclassified as rate-limit.","commonSituations":"Skill catalog refresh in a CI/shared-IP environment where the 60/hour budget is shared; recursive tree fetches plus many blob fetches; many users behind one NAT syncing at once; no GITHUB token configured.","solutions":["Set a GITHUB token (raises limit to 5000/hour) for skill syncing, especially in shared/CI environments.","Honor Retry-After / X-RateLimit-Reset headers and back off instead of retrying immediately.","Rely on getCached/setCache to avoid repeated fetches of the same URL within the TTL.","Distinguish true rate-limit (X-RateLimit-Remaining: 0) from forbidden-resource 403 to give accurate guidance."],"exampleFix":"// before\nif (response.status === 403) throw new GitHubApiError('GitHub API rate limit exceeded. Try again later.', 403)\n\n// after: honor reset header\nif (response.status === 403) {\n  const reset = Number(response.headers.get('x-ratelimit-reset')) * 1000\n  throw new GitHubApiError(`GitHub API rate limit exceeded. Resets at ${new Date(reset).toISOString()}`, 403)\n}","handlingStrategy":"retry","validationCode":"if (!process.env.GITHUB_TOKEN && sharedEnvironment) warn('Unauthenticated GitHub budget is 60/hour/IP')","typeGuard":"function isGitHubRateLimited(e: unknown): e is GitHubApiError { return e instanceof GitHubApiError && e.status === 403 }","tryCatchPattern":"try { return await githubFetch(url) } catch (e) {\n  if (isGitHubRateLimited(e)) { const reset = getRateLimitReset(); await delay(until reset); return githubFetch(url) }\n  throw e\n}","preventionTips":["Set GITHUB_TOKEN (5000/hour) for skill syncing, especially on shared IPs.","Rely on getCached/setCache to avoid repeat fetches.","Honor X-RateLimit-Reset/Retry-After instead of immediate retries.","Distinguish rate-limit 403 from forbidden-resource 403."],"tags":["skills","github-fetcher","http","rate-limit","typescript"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}