chatboxai/chatbox · error · GitHubApiError

GitHub API error: ${response.status} ${response.statusText}

Error message

GitHub API error: ${response.status} ${response.statusText}

What it means

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.

Source

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

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}`)
    return null
  }

View on GitHub (pinned to 81571269ad)

Solutions

  1. Verify the owner/repo slug is correct and that the repository is public (or add a token-backed fetch path).
  2. Check https://www.githubstatus.com for an active GitHub API incident if the status starts with 5.
  3. 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.
  4. If recurring, switch to an authenticated request path so the limit is 5000/hr instead of 60/hr.

Example fix

// before
throw new GitHubApiError(`GitHub API error: ${response.status} ${response.statusText}`, response.status)

// after — distinguish retryable 5xx from permanent 4xx so callers can back off
const isRetryable = response.status >= 500
throw new GitHubApiError(
  `GitHub API error: ${response.status} ${response.statusText}`,
  response.status,
  isRetryable
)
Defensive patterns

Strategy: retry

Validate before calling

// Before scanning, sanity-check the slug and that the user has rate-limit budget.
function isValidRepoSlug(owner: string, repo: string): boolean {
  return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(owner) && /^[A-Za-z0-9_.-]{1,100}$/.test(repo)
}
if (!isValidRepoSlug(owner, repo)) throw new Error('Invalid owner/repo slug')

Type guard

import { GitHubApiError } from './github-fetcher'
function isGitHubApiError(e: unknown): e is GitHubApiError {
  return e instanceof Error && (e as GitHubApiError).name === 'GitHubApiError' && typeof (e as GitHubApiError).statusCode === 'number'
}
function isRetryableStatus(code: number): boolean {
  return code === 429 || code >= 500
}

Try / catch

try {
  await detectSkillsInRepo(owner, repo)
} catch (error) {
  if (isGitHubApiError(error) && isRetryableStatus(error.statusCode)) {
    clearCache()
    await backoffRetry(() => detectSkillsInRepo(owner, repo))
  } else if (isGitHubApiError(error) && error.statusCode === 404) {
    showUser('Repository not found')
  } else {
    throw error
  }
}

Prevention

When it happens

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

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

Related errors


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