chatboxai/chatbox · warning · GitHubApiError

GitHub API rate limit exceeded. Try again later.

Error message

GitHub API rate limit exceeded. Try again later.

What it means

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.

Source

Thrown at src/main/skills/github-fetcher.ts:90

}

async function githubFetch<T>(url: string): Promise<T> {
  const cached = getCached<T>(url)
  if (cached !== undefined) return cached

  const response = await fetch(url, {
    headers: {
      'User-Agent': USER_AGENT,
      Accept: 'application/vnd.github.v3+json',
    },
  })

  if (!response.ok) {
    if (response.status === 404) {
      throw new GitHubApiError(`Not found: ${url}`, 404)
    }
    if (response.status === 403) {
      throw new GitHubApiError('GitHub API rate limit exceeded. Try again later.', 403)
    }
    throw new GitHubApiError(`GitHub API error: ${response.status} ${response.statusText}`, response.status)
  }

  const data = (await response.json()) as T
  setCache(url, data)
  return data
}

async function fetchRepoTree(owner: string, repo: string): Promise<GitHubTreeResponse | null> {
  const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`
  const result = await githubFetch<GitHubTreeResponse>(url)
  if (!Array.isArray(result?.tree)) {
    log.warn(`Unexpected tree response shape for ${owner}/${repo}`)
    return null
  }
  if (result.truncated) {
    log.warn(`Tree listing truncated for ${owner}/${repo}`)

View on GitHub (pinned to 81571269ad)

Solutions

  1. Set a GITHUB token (raises limit to 5000/hour) for skill syncing, especially in shared/CI environments.
  2. Honor Retry-After / X-RateLimit-Reset headers and back off instead of retrying immediately.
  3. Rely on getCached/setCache to avoid repeated fetches of the same URL within the TTL.
  4. Distinguish true rate-limit (X-RateLimit-Remaining: 0) from forbidden-resource 403 to give accurate guidance.

Example fix

// before
if (response.status === 403) throw new GitHubApiError('GitHub API rate limit exceeded. Try again later.', 403)

// after: honor reset header
if (response.status === 403) {
  const reset = Number(response.headers.get('x-ratelimit-reset')) * 1000
  throw new GitHubApiError(`GitHub API rate limit exceeded. Resets at ${new Date(reset).toISOString()}`, 403)
}
Defensive patterns

Strategy: retry

Validate before calling

if (!process.env.GITHUB_TOKEN && sharedEnvironment) warn('Unauthenticated GitHub budget is 60/hour/IP')

Type guard

function isGitHubRateLimited(e: unknown): e is GitHubApiError { return e instanceof GitHubApiError && e.status === 403 }

Try / catch

try { return await githubFetch(url) } catch (e) {
  if (isGitHubRateLimited(e)) { const reset = getRateLimitReset(); await delay(until reset); return githubFetch(url) }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/f9ce6519d91a92fd. Report an issue: GitHub.