Skip to content

fix: explain latest-release fetch failures and use GITHUB_TOKEN - #7

Merged
Andrew Grathwohl (agrathwohl) merged 2 commits into
mainfrom
fix/latest-release-errors-and-token
Nov 5, 2026
Merged

Andrew Grathwohl (agrathwohl) merged 2 commits into
mainfrom
fix/latest-release-errors-and-token

Conversation

@jerodsanto

@jerodsanto Jerod Santo (jerodsanto) commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On a first run (no cached binary), if the call to the GitHub releases API fails, sfw exits with:

[sfw] Failed to prepare firewall binary: Unable to fetch latest release and no valid cached release found.

The underlying error was discarded (catch (_e)), so there was no way to tell a rate limit from a DNS, TLS, or proxy failure. The most likely cause in practice is rate limiting: the call is unauthenticated, and GitHub caps those at 60/hour per IP, which shared CI runners and NAT'd networks exhaust quickly.

Changes

  • Keep the real error. It's attached as cause and appended to the message, e.g.
    … no valid cached release found: getaddrinfo ENOTFOUND api.github.com
  • Call out rate limiting. On a 429, or a 403 carrying GitHub's rate-limit headers (x-ratelimit-remaining: 0 or retry-after), the message includes the reset time and suggests setting GITHUB_TOKEN. Other 403s (e.g. from a proxy) are reported as-is.
  • Use a token when available. GITHUB_TOKEN or GH_TOKEN is sent to the releases API. If GitHub rejects the token (401), the request is retried anonymously so a stale token can't break an install that works today.
  • Don't leak the token on redirects. httpsGet now drops Authorization when a redirect goes to a different host, and resolves relative Location headers (previously these would throw).
  • http.ts now rejects with an HttpError carrying the status code and response headers.

The background update check (when a cached binary exists) still swallows errors, as before.

Testing

  • npm run check and npm run build pass.
  • Checked the redirect header handling, token header, 401 fallback, and error messages against a stubbed https.get.
  • Ran the built dist/sfw.mjs with an empty cache:
    • GITHUB_TOKEN=<bogus> → got a 401, retried anonymously, downloaded and ran sfw-free v1.16.0.
    • Unresolvable API host → printed the new detailed error.
  • The rate-limit message was only checked with stubbed headers, not against a real limit.

Note: tsc already fails on main for process.env.SFW_SKIP_UPDATE_CHECK (noPropertyAccessFromIndexSignature), and the new process.env reads follow the same dot-access style, since Biome's useLiteralKeys rejects bracket access. CI doesn't run tsc.

🤖 Generated with Claude Code


Note

Low Risk
Changes are limited to GitHub release metadata fetching and HTTP client behavior for downloads; no auth or business-logic paths, with a security-conscious redirect fix.

Overview
First-run installs no longer fail with a generic “unable to fetch latest release” message when the GitHub releases API errors. The underlying failure is preserved as cause and surfaced via describeFetchError, including tailored guidance when GitHub rate-limits (429 or rate-limit 403) with reset time and a GITHUB_TOKEN / GH_TOKEN hint.

fetchLatest now sends a bearer token from those env vars when set, and retries without auth if the token gets a 401 so a bad token does not block public installs.

httpsGet rejects with a structured HttpError (status + headers) instead of a plain message, resolves relative redirect Location values, and strips Authorization on redirects to a different host so tokens are not sent to release CDNs.

Reviewed by Cursor Bugbot for commit 0004a91. Configure here.

When no cached binary exists and the GitHub API call fails, the wrapper
threw a generic "Unable to fetch latest release" error and discarded the
cause, leaving users no way to tell a rate limit from a DNS or proxy
failure.

- Keep the underlying error (as `cause` and in the message), and call out
  GitHub API rate limiting with the reset time and a GITHUB_TOKEN hint.
- Send GITHUB_TOKEN/GH_TOKEN to the releases API when set, so CI and
  shared IPs aren't capped at 60 anonymous requests/hour. A rejected
  token (401) falls back to an anonymous request.
- Drop the Authorization header on cross-host redirects, and resolve
  relative Location headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but none of the reported issues met your autofix severity threshold.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0004a91. Configure here.

Comment thread src/github.ts Resolved
Comment thread src/github.ts Resolved
- Prefer `retry-after` for secondary limits. `x-ratelimit-reset` is sent
  on every response as the primary hourly window, so only show it once
  `x-ratelimit-remaining` is 0.
- Base the token hint on whether the failing request sent Authorization
  (now recorded on HttpError), not on whether a token is in the
  environment. After a 401 anonymous retry, a rate-limited request was
  anonymous and should get the "set GITHUB_TOKEN" hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cross-host Authorization strip and the 401 fallback were only checked against an uncommitted stub, and the repo has no test runner, so a later refactor can reopen the token leak unnoticed. Commit a small node:test file that stubs https.get and run it in CI.

Comment thread src/github.ts Resolved
Comment thread src/github.ts Resolved
Comment thread src/http.ts Resolved
@agrathwohl
Andrew Grathwohl (agrathwohl) merged commit df21a11 into main Nov 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants