{"record":{"id":"3427a597a15c0eee","repo":"jamiepine/voicebox","slug":"failed-to-fetch-stars","errorCode":null,"errorMessage":"Failed to fetch stars","messagePattern":"Failed to fetch stars","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"landing/src/components/Navbar.tsx","lineNumber":22,"sourceCode":"import Image from 'next/image';\nimport { useEffect, useState } from 'react';\nimport { DONATE_URL, GITHUB_REPO, TOKEN_TICKER } from '@/lib/constants';\n\nfunction formatStarCount(count: number): string {\n  if (count >= 1000) {\n    const k = count / 1000;\n    return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`;\n  }\n  return count.toString();\n}\n\nexport function Navbar() {\n  const [starCount, setStarCount] = useState<number | null>(null);\n\n  useEffect(() => {\n    fetch('/api/stars')\n      .then((res) => {\n        if (!res.ok) throw new Error('Failed to fetch stars');\n        return res.json();\n      })\n      .then((data) => {\n        if (typeof data.count === 'number') setStarCount(data.count);\n      })\n      .catch((error) => {\n        console.error('Failed to fetch star count:', error);\n      });\n  }, []);\n\n  return (\n    <nav className=\"fixed inset-x-0 top-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-xl\">\n      <div className=\"mx-auto flex max-w-7xl items-center justify-between px-6 py-3 sm:grid sm:grid-cols-[1fr_auto_1fr] sm:gap-x-6\">\n        {/* Logo + wordmark */}\n        <a href=\"/\" className=\"flex items-center gap-2.5 justify-self-start\">\n          <Image\n            src=\"/voicebox-logo-app.webp\"\n            alt=\"Voicebox\"","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/landing/src/components/Navbar.tsx#L4-L40","documentation":"Thrown in the Navbar useEffect when /api/stars returns non-2xx; caught and console.erroled, leaving starCount null so the badge is hidden. /api/stars proxies getStarCount() which fetches api.github.com/repos/<repo> for stargazers_count with a 10-minute revalidate.","triggerScenarios":"GET /api/stars returns 500 because getStarCount() threw (GitHub 403/429 rate limit, repo not found, network). Every page rendering the Navbar triggers the fetch, multiplying rate-limit pressure.","commonSituations":"Unauthenticated GitHub rate limit (60/hr) exhausted because the Navbar mounts on every landing route. GITHUB_REPO pointing at a renamed/private repo. Deploy IP blocked by GitHub abuse detection.","solutions":["Add a server-side GitHub token to the getStarCount() fetch to raise the rate limit.","Increase the revalidate window (currently 600s) and ensure the Navbar relies on cached data.","Hide the star badge gracefully when starCount is null (already the behavior).","Verify GITHUB_REPO in releases.ts is correct and public."],"exampleFix":"// before\nif (!res.ok) throw new Error('Failed to fetch stars');\n// after\nif (!res.ok) { console.warn('stars status', res.status); return; }","handlingStrategy":"fallback","validationCode":null,"typeGuard":"function isStarPayload(v: unknown): v is { count: number } {\n  return typeof v === 'object' && v !== null && typeof (v as { count?: unknown }).count === 'number';\n}","tryCatchPattern":"try {\n  const res = await fetch('/api/stars');\n  if (!res.ok) throw new Error('Failed to fetch stars');\n  const data = await res.json();\n  if (typeof data.count === 'number') setStarCount(data.count);\n} catch (error) {\n  console.error('Failed to fetch star count:', error);\n  // starCount stays null; badge hidden\n}","preventionTips":["Hide the star badge when starCount is null.","Add a server-side GitHub token; the Navbar mounts on every route and exhausts the 60/hr cap fast.","Increase the 600s revalidate and lean on cached data."],"tags":["network","external-api","github","nextjs","navbar","landing"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}