{"record":{"id":"7db5f96f10d69fbb","repo":"jamiepine/voicebox","slug":"github-api-error-response-status","errorCode":null,"errorMessage":"GitHub API error: ${response.status}","messagePattern":"GitHub API error: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"landing/src/lib/releases.ts","lineNumber":46,"sourceCode":" * Fetches the latest release from GitHub and extracts download links\n */\nexport async function getLatestRelease(): Promise<ReleaseInfo> {\n  // Return cached data if still valid\n  const now = Date.now();\n  if (cachedReleaseInfo && now - cacheTimestamp < CACHE_DURATION) {\n    return cachedReleaseInfo;\n  }\n\n  try {\n    const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {\n      cache: 'no-store',\n      headers: {\n        Accept: 'application/vnd.github.v3+json',\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`GitHub API error: ${response.status}`);\n    }\n\n    const release = await response.json();\n    const version = release.tag_name;\n    const assets = release.assets || [];\n\n    // Extract download links based on file patterns\n    const downloadLinks: Partial<DownloadLinks> = {};\n\n    for (const asset of assets) {\n      const name = asset.name.toLowerCase();\n      const url = asset.browser_download_url;\n\n      // Skip signature files and other non-downloadable files\n      if (name.endsWith('.sig') || name.endsWith('.json') || name.endsWith('.txt')) {\n        continue;\n      }\n","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/landing/src/lib/releases.ts#L28-L64","documentation":"Thrown by `getLatestRelease()` in `landing/src/lib/releases.ts` (line 45-47) when GitHub's `GET /repos/jamiepine/voicebox/releases/latest` returns non-2xx. The call is unauthenticated (`cache: 'no-store'`, no Authorization header), so it is subject to GitHub's 60-requests-per-hour-per-IP unauthenticated rate limit. There is a 5-minute in-memory cache that absorbs repeat load, but a cold cache during heavy traffic still hits the limit.","triggerScenarios":"Unauthenticated rate limit exceeded (HTTP 403 with `X-RateLimit-Remaining: 0`); the repo was renamed/moved so the path 404s; the repo has no published releases so `releases/latest` returns 404; GitHub API outage (5xx); network/DNS failure reaching api.github.com.","commonSituations":"A traffic spike to the landing page exhausts the 60 req/hour limit from a shared egress IP (CDN/NAT); repo was renamed and the constant `GITHUB_REPO` is stale; project has never published a GitHub Release (only tags); shared hosting NAT means many sites share one IP's quota.","solutions":["Add a GitHub Personal Access Token (fine-grained, public-read) as an `Authorization: Bearer <token>` header to lift the limit to 5000/hour; store it server-side as an env var.","Honor `X-RateLimit-Reset` on a 403 and serve the last cached `cachedReleaseInfo` (extend the cache to survive rate-limit windows).","If the repo has no latest release, fall back to listing `/releases` and pick the first, or surface 'no release available'.","Keep `GITHUB_REPO` in sync with the actual repo slug after any rename."],"exampleFix":"// before\nconst response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {\n  cache: 'no-store',\n  headers: { Accept: 'application/vnd.github.v3+json' },\n});\n\n// after — authenticated + rate-limit aware\nconst headers: Record<string,string> = { Accept: 'application/vnd.github.v3+json' };\nif (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;\nconst response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {\n  next: { revalidate: 300 },\n  headers,\n});\nif (response.status === 403 && cachedReleaseInfo) return cachedReleaseInfo;","handlingStrategy":"fallback","validationCode":"// Authenticate when a token is available, and reuse the cache\nconst headers: Record<string,string> = { Accept: 'application/vnd.github.v3+json' };\nif (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;","typeGuard":"function isRateLimited(res: Response): boolean {\n  return res.status === 403\n    && res.headers.get('X-RateLimit-Remaining') === '0';\n}","tryCatchPattern":"try {\n  return await getLatestRelease();\n} catch (e) {\n  if (cachedReleaseInfo) return cachedReleaseInfo; // stale cache beats nothing\n  return null; // render a 'release info unavailable' state\n}","preventionTips":["Add a server-side `GITHUB_TOKEN` to move off the 60/hour unauthenticated limit.","Serve the stale `cachedReleaseInfo` on 403/rate-limit instead of throwing.","Keep `GITHUB_REPO` in sync after any repo rename.","Honor `X-RateLimit-Reset` to back off correctly."],"tags":["github-api","rate-limit","network","landing"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}