janhq/jan · warning · Error

Failed to fetch releases

Error message

Failed to fetch releases

What it means

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.

Source

Thrown at web-app/src/hooks/useReleaseNotes.ts:30

type ReleaseState = {
  release: Release | null
  loading: boolean
  error: string | null
  fetchLatestRelease: (includeBeta: boolean) => Promise<void>
}

export const useReleaseNotes = create<ReleaseState>((set) => ({
  release: null,
  loading: false,
  error: null,

  fetchLatestRelease: async (includeBeta: boolean) => {
    set({ loading: true, error: null })
    try {
      const res = await fetch(
        'https://api.github.com/repos/janhq/jan/releases'
      )
      if (!res.ok) throw new Error('Failed to fetch releases')
      const releases = await res.json()

      const stableRelease = releases.find(
        (release: { prerelease: boolean; draft: boolean }) =>
          !release.prerelease && !release.draft
      )
      const betaRelease = releases.find(
        (release: { prerelease: boolean }) => release.prerelease
      )

      const selected = includeBeta
        ? (betaRelease ?? stableRelease)
        : stableRelease
      set({ release: selected, loading: false })
    } catch (err: any) {
      set({ error: err.message, loading: false })
    }
  },

View on GitHub (pinned to fad3f12a14)

Solutions

  1. Add a conditional-request (ETag/Last-Modified) or cache the result to reduce calls below the rate limit.
  2. Attach a GitHub token (even a public-read token raises the limit to 5000/hr).
  3. Fall back to the releases HTML page or a CDN-hosted version file.
  4. Handle 403 specifically with a 'rate-limited, try later' message instead of a generic failure.

Example fix

// before
const res = await fetch('https://api.github.com/repos/janhq/jan/releases')
if (!res.ok) throw new Error('Failed to fetch releases')

// after
const headers: Record<string, string> = {}
if (import.meta.env.VITE_GITHUB_TOKEN) {
  headers.Authorization = `Bearer ${import.meta.env.VITE_GITHUB_TOKEN}`
}
const res = await fetch('https://api.github.com/repos/janhq/jan/releases', { headers })
if (!res.ok) {
  throw new Error(
    res.status === 403
      ? 'GitHub API rate limit reached. Try again later.'
      : `Failed to fetch releases (HTTP ${res.status})`
  )
}
Defensive patterns

Strategy: retry

Validate before calling

// Cache the last successful fetch timestamp to avoid hammering the API.
const last = localStorage.getItem('lastReleaseFetch')
if (last && Date.now() - Number(last) < 60 * 60 * 1000) return // within an hour

Type guard

function isRateLimited(res: Response): boolean {
  return res.status === 403 || res.status === 429
}

Try / catch

try {
  const res = await fetch('https://api.github.com/repos/janhq/jan/releases', {
    headers: token ? { Authorization: `Bearer ${token}` } : {},
  })
  if (!res.ok) {
    if (isRateLimited(res)) {
      set({ error: 'GitHub rate limit reached. Try again later.', loading: false })
      return
    }
    throw new Error(`Failed to fetch releases (HTTP ${res.status})`)
  }
  // proceed
} catch (err: any) {
  set({ error: err.message, loading: false })
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/33e0da01681c5501. Report an issue: GitHub.