{"record":{"id":"664ad38f3f4cbead","repo":"aaif-goose/goose","slug":"github-api-returned-response-status-response","errorCode":null,"errorMessage":"GitHub API returned ${response.status}: ${response.statusText}","messagePattern":"GitHub API returned (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ui/desktop/src/utils/githubUpdater.ts","lineNumber":67,"sourceCode":"\n      const response = await fetch(this.apiUrl, {\n        headers: {\n          Accept: 'application/vnd.github.v3+json',\n          'User-Agent': `Goose-Desktop/${app.getVersion()}`,\n        },\n        signal: controller.signal,\n      });\n\n      clearTimeout(timeoutId);\n      const fetchDuration = Date.now() - startTime;\n      log.info(\n        `GitHubUpdater: GitHub API response status: ${response.status} ${response.statusText} (took ${fetchDuration}ms)`\n      );\n\n      if (!response.ok) {\n        const errorText = await response.text();\n        log.error(`GitHubUpdater: GitHub API error response: ${errorText}`);\n        throw new Error(`GitHub API returned ${response.status}: ${response.statusText}`);\n      }\n\n      const release: GitHubRelease = await safeJsonParse<GitHubRelease>(\n        response,\n        'Failed to get GitHub release information'\n      );\n      log.info(`GitHubUpdater: Found release: ${release.tag_name} (${release.name})`);\n      log.info(`GitHubUpdater: Release published at: ${release.published_at}`);\n      log.info(`GitHubUpdater: Release assets count: ${release.assets.length}`);\n\n      const latestVersion = release.tag_name.replace(/^v/, ''); // Remove 'v' prefix if present\n      const currentVersion = app.getVersion();\n\n      log.info(\n        `GitHubUpdater: Current version: ${currentVersion}, Latest version: ${latestVersion}`\n      );\n\n      // Compare versions","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/ui/desktop/src/utils/githubUpdater.ts#L49-L85","documentation":"githubUpdater fetches the latest release from the GitHub releases API; any non-2xx response throws with the HTTP status and status text. The response body is logged before throwing. The two dominant causes are rate limiting (403 with X-RateLimit-Remaining: 0 for unauthenticated clients — 60 req/hour per IP) and a 404 because the configured owner/repo is wrong or the release is private without a token.","triggerScenarios":"checkForUpdates via GitHub fallback after electron-updater failed; more than 60 unauthenticated API calls/hour (shared NAT IPs amplify this); wrong GITHUB owner/repo in updater config; private repo fetched without authorization; GitHub incident returning 5xx.","commonSituations":"Corporate NAT pooling many users behind one IP; misconfigured repo after a rename/transfer; CI runs hammering the API; release not yet published when the client checks.","solutions":["If 403: wait for the rate-limit window to reset (check X-RateLimit-Reset) or supply an authenticated request/GITHUB_TOKEN so the limit rises to 5000/h","If 404: verify the owner/repo the updater queries and that the latest release exists and is public","If 5xx: retry with backoff — GitHub API transient failures are common","Cache release-check results to avoid re-querying on every launch"],"exampleFix":"// before\nconst response = await fetch(releaseApiUrl);\n// after (fail fast on rate limit, keep body details)\nconst response = await fetch(releaseApiUrl);\nif (response.status === 403 || response.status === 429) {\n  const reset = response.headers.get('x-ratelimit-reset');\n  log.warn(`Rate limited until ${reset}`);\n}","handlingStrategy":"retry","validationCode":"// Check rate-limit headers before treating failure as fatal:\nconst response = await fetch(apiUrl);\nconst remaining = Number(response.headers.get('x-ratelimit-remaining') ?? '1');\nif (remaining <= 1) scheduleNextCheckAfterReset(response.headers.get('x-ratelimit-reset'));","typeGuard":null,"tryCatchPattern":"async function checkWithBackoff(retries = 3): Promise<GitHubRelease> {\n  for (let i = 0; i <= retries; i++) {\n    try {\n      return await checkLatestRelease();\n    } catch (e) {\n      const s = String(e);\n      const rateLimited = s.includes(' 403') || s.includes(' 429');\n      if (i === retries || (rateLimited && !hasToken)) throw e;\n      await new Promise(r => setTimeout(r, 2 ** i * 1000));\n    }\n  }\n  throw new Error('unreachable');\n}","preventionTips":["Cache release metadata with a TTL instead of checking every launch/hour","Send an Authorization header in managed deployments to lift the 60/hr unauthenticated cap","Treat 404 as config error (check owner/repo) and 403/429 as rate limit — different remedies"],"tags":["github-api","rate-limit","network","auto-update","http"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}