jamiepine/voicebox · warning · Error
GitHub API error: ${response.status}
Error message
GitHub API error: ${response.status} What it means
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.
Source
Thrown at landing/src/lib/releases.ts:46
* Fetches the latest release from GitHub and extracts download links
*/
export async function getLatestRelease(): Promise<ReleaseInfo> {
// Return cached data if still valid
const now = Date.now();
if (cachedReleaseInfo && now - cacheTimestamp < CACHE_DURATION) {
return cachedReleaseInfo;
}
try {
const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {
cache: 'no-store',
headers: {
Accept: 'application/vnd.github.v3+json',
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const release = await response.json();
const version = release.tag_name;
const assets = release.assets || [];
// Extract download links based on file patterns
const downloadLinks: Partial<DownloadLinks> = {};
for (const asset of assets) {
const name = asset.name.toLowerCase();
const url = asset.browser_download_url;
// Skip signature files and other non-downloadable files
if (name.endsWith('.sig') || name.endsWith('.json') || name.endsWith('.txt')) {
continue;
}
View on GitHub (pinned to 51f49dea19)
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.
Example fix
// before
const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {
cache: 'no-store',
headers: { Accept: 'application/vnd.github.v3+json' },
});
// after — authenticated + rate-limit aware
const headers: Record<string,string> = { Accept: 'application/vnd.github.v3+json' };
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {
next: { revalidate: 300 },
headers,
});
if (response.status === 403 && cachedReleaseInfo) return cachedReleaseInfo; Defensive patterns
Strategy: fallback
Validate before calling
// Authenticate when a token is available, and reuse the cache
const headers: Record<string,string> = { Accept: 'application/vnd.github.v3+json' };
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; Type guard
function isRateLimited(res: Response): boolean {
return res.status === 403
&& res.headers.get('X-RateLimit-Remaining') === '0';
} Try / catch
try {
return await getLatestRelease();
} catch (e) {
if (cachedReleaseInfo) return cachedReleaseInfo; // stale cache beats nothing
return null; // render a 'release info unavailable' state
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- RPC ${method} HTTP ${res.status}
- Failed to fetch releases
- releases ${r.status}
- Failed to fetch releases
- Failed to fetch stars
AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12).
Data as JSON: /api/errors/7db5f96f10d69fbb.
Report an issue: GitHub.