{"record":{"id":"9b656b94497c6b9f","repo":"Budibase/budibase","slug":"github-stars-missing","errorCode":null,"errorMessage":"GitHub stars missing","messagePattern":"GitHub stars missing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/worker/src/api/controllers/global/github.ts","lineNumber":45,"sourceCode":"\n  try {\n    const response = await fetch(GITHUB_REPO_URL, {\n      headers: {\n        Accept: \"application/vnd.github+json\",\n        \"User-Agent\": USER_AGENT,\n      },\n      timeout: GITHUB_TIMEOUT_MS,\n    })\n\n    if (!response.ok) {\n      throw new Error(`GitHub response: ${response.status}`)\n    }\n\n    const json = (await response.json()) as { stargazers_count?: number }\n    const stars = json.stargazers_count\n\n    if (typeof stars !== \"number\") {\n      throw new Error(\"GitHub stars missing\")\n    }\n\n    const value: GetGitHubStarsResponse = {\n      stars,\n      fetchedAt: new Date().toISOString(),\n    }\n    const toStore: StarsCacheEnvelope = {\n      value,\n      expiresAt: Date.now() + CACHE_TTL_MS,\n    }\n\n    await cache.store(CACHE_KEY, toStore, RETENTION_TTL_SECONDS, {\n      useTenancy: false,\n    })\n\n    ctx.body = value\n  } catch (err) {\n    console.error(\"Failed to fetch GitHub stars\", err)","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/worker/src/api/controllers/global/github.ts#L27-L63","documentation":"getStars fetches the GitHub repo metadata and expects `stargazers_count` in the JSON response. If the field is missing or not a number, it throws \"GitHub stars missing\" rather than returning invalid data. This guards the GetGitHubStarsResponse contract so downstream consumers always get a numeric star count.","triggerScenarios":"Calling getStars when the GitHub API returns a response body without a numeric `stargazers_count` — e.g. a rate-limit response ({\"message\": \"API rate limit exceeded\"}), an error object, a 404 body for a moved/renamed repo, or any non-200 response that still parses as JSON.","commonSituations":"GitHub API rate limiting (unauthenticated requests are capped at 60/hour per IP), the configured repo URL pointing at a nonexistent or renamed repository, GitHub incidents returning error payloads, or a proxy/firewall returning an HTML/JSON error page.","solutions":["Check the HTTP status before parsing; handle 403/429 rate-limit responses separately","Authenticate GitHub API calls with a GITHUB_TOKEN to raise the rate limit","Verify the repository URL/owner/name configured for the stars endpoint is correct","Retry with backoff if GitHub is having an incident; cache the last successful star count"],"exampleFix":"// before\nconst json = (await response.json()) as { stargazers_count?: number }\n// after\nif (!response.ok) {\n  throw new Error(`GitHub API request failed: ${response.status}`)\n}\nconst json = (await response.json()) as { stargazers_count?: number }","handlingStrategy":"type-guard","validationCode":"const res = await fetch(githubApiUrl)\nif (!res.ok) throw new Error(`GitHub API ${res.status}`)","typeGuard":"function hasStars(json: unknown): json is { stargazers_count: number } {\n  return typeof json === \"object\" && json !== null &&\n    typeof (json as { stargazers_count?: unknown }).stargazers_count === \"number\"\n}","tryCatchPattern":"try {\n  const stars = await getStars()\n} catch (e) {\n  const stars = cachedStars // fall back to last known value\n}","preventionTips":["Authenticate GitHub API requests to avoid rate limits","Check response.ok before parsing JSON","Cache the last successful star count and serve stale data on failure"],"tags":["network","api","github","rate-limit"],"backgroundTag":"github-api-rate-limit","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}