{"record":{"id":"33e0da01681c5501","repo":"janhq/jan","slug":"failed-to-fetch-releases","errorCode":null,"errorMessage":"Failed to fetch releases","messagePattern":"Failed to fetch releases","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"web-app/src/hooks/useReleaseNotes.ts","lineNumber":30,"sourceCode":"type ReleaseState = {\n  release: Release | null\n  loading: boolean\n  error: string | null\n  fetchLatestRelease: (includeBeta: boolean) => Promise<void>\n}\n\nexport const useReleaseNotes = create<ReleaseState>((set) => ({\n  release: null,\n  loading: false,\n  error: null,\n\n  fetchLatestRelease: async (includeBeta: boolean) => {\n    set({ loading: true, error: null })\n    try {\n      const res = await fetch(\n        'https://api.github.com/repos/janhq/jan/releases'\n      )\n      if (!res.ok) throw new Error('Failed to fetch releases')\n      const releases = await res.json()\n\n      const stableRelease = releases.find(\n        (release: { prerelease: boolean; draft: boolean }) =>\n          !release.prerelease && !release.draft\n      )\n      const betaRelease = releases.find(\n        (release: { prerelease: boolean }) => release.prerelease\n      )\n\n      const selected = includeBeta\n        ? (betaRelease ?? stableRelease)\n        : stableRelease\n      set({ release: selected, loading: false })\n    } catch (err: any) {\n      set({ error: err.message, loading: false })\n    }\n  },","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/janhq/jan/blob/fad3f12a147d138388a66f0d92a02b2675f65294/web-app/src/hooks/useReleaseNotes.ts#L12-L48","documentation":"Thrown when `fetch('https://api.github.com/repos/janhq/jan/releases')` returns a non-OK response. GitHub's unauthenticated API is rate-limited to 60 requests/hour per IP, and 403 is the most common non-OK status. No auth token is attached, so this is purely the anonymous endpoint.","triggerScenarios":"Anonymous GitHub API rate limit exceeded (60/hr/IP) — returns 403 with a rate-limit body; the repo was renamed/moved (301/404); GitHub API outage (5xx); network/proxy blocking api.github.com.","commonSituations":"Shared NAT/office IP where many users exhaust the 60/hr budget; corporate proxy; GitHub incident; development machines hitting the endpoint on every app launch.","solutions":["Add a conditional-request (ETag/Last-Modified) or cache the result to reduce calls below the rate limit.","Attach a GitHub token (even a public-read token raises the limit to 5000/hr).","Fall back to the releases HTML page or a CDN-hosted version file.","Handle 403 specifically with a 'rate-limited, try later' message instead of a generic failure."],"exampleFix":"// before\nconst res = await fetch('https://api.github.com/repos/janhq/jan/releases')\nif (!res.ok) throw new Error('Failed to fetch releases')\n\n// after\nconst headers: Record<string, string> = {}\nif (import.meta.env.VITE_GITHUB_TOKEN) {\n  headers.Authorization = `Bearer ${import.meta.env.VITE_GITHUB_TOKEN}`\n}\nconst res = await fetch('https://api.github.com/repos/janhq/jan/releases', { headers })\nif (!res.ok) {\n  throw new Error(\n    res.status === 403\n      ? 'GitHub API rate limit reached. Try again later.'\n      : `Failed to fetch releases (HTTP ${res.status})`\n  )\n}","handlingStrategy":"retry","validationCode":"// Cache the last successful fetch timestamp to avoid hammering the API.\nconst last = localStorage.getItem('lastReleaseFetch')\nif (last && Date.now() - Number(last) < 60 * 60 * 1000) return // within an hour","typeGuard":"function isRateLimited(res: Response): boolean {\n  return res.status === 403 || res.status === 429\n}","tryCatchPattern":"try {\n  const res = await fetch('https://api.github.com/repos/janhq/jan/releases', {\n    headers: token ? { Authorization: `Bearer ${token}` } : {},\n  })\n  if (!res.ok) {\n    if (isRateLimited(res)) {\n      set({ error: 'GitHub rate limit reached. Try again later.', loading: false })\n      return\n    }\n    throw new Error(`Failed to fetch releases (HTTP ${res.status})`)\n  }\n  // proceed\n} catch (err: any) {\n  set({ error: err.message, loading: false })\n}","preventionTips":["Cache release data and use conditional requests (ETag/If-None-Match).","Attach a GitHub token to raise the rate limit from 60 to 5000/hr.","Handle 403/429 specifically with a friendly rate-limit message.","Fall back to a CDN-hosted version manifest."],"tags":["github-api","rate-limit","release-notes","network"],"backgroundTag":null,"analyzedSha":"fad3f12a147d138388a66f0d92a02b2675f65294","analyzedAt":"2026-08-12T20:33:47.516Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}