Skip to content

fix(selfupdate): report a rate-limited 403 as rate limiting, not a private repo - #126

Open
gustavobertoi wants to merge 1 commit into
mainfrom
fix/github-rate-limit-message
Open

fix(selfupdate): report a rate-limited 403 as rate limiting, not a private repo#126
gustavobertoi wants to merge 1 commit into
mainfrom
fix/github-rate-limit-message

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

The report

$ ds self update
  Github API https://api.github.com/repos/open-source-cloud/devstack/releases
  returned 403 Forbidden (set GITHUB_TOKEN if the repo is private).

The repository is public. The actual cause was the anonymous GitHub API
quota (60 requests/hour per IP) being spent:

x-ratelimit-limit: 60
x-ratelimit-remaining: 0

GitHub answers an exhausted quota with a plain 403 Forbidden, which is
indistinguishable from a permissions failure unless you read the headers. Our
message assumed the permissions case for every 403, so it described a problem
that did not exist and hid the one that did — including that it clears itself in
minutes.

The fix

apiError now separates four cases on X-RateLimit-Remaining: 0 (plus
Retry-After, which is how secondary limits are signalled):

case message
rate limited names the limit hit, says plainly it is not a permissions problem, gives the reset countdown, still offers GITHUB_TOKEN (it raises the cap to 5000/h)
401 the token is set but was rejected
404 keeps the private-repo hint — this is where it belongs
other the bare status

After:

GitHub API rate limit exceeded (60 requests/hour), resets in 10m0s.
This is not a permissions problem — the repository is reachable, you have
simply used up the anonymous quota for your IP.
Set GITHUB_TOKEN (or GH_TOKEN) to raise the limit to 5000 requests/hour: <url>

Applied to both the API path (selfupdate.go) and the asset-download path
(update.go) — the reported failure hit the asset download first, and that path
had its own, even barer, message.

install.sh cannot read the headers, because its api() helper uses curl -f
which discards the error body. It instead probes /rate_limit (which does not
count against the quota) and appends the same clarification.

Notes

  • The reset countdown is omitted rather than rendered when the stamp is in
    the past or unparseable, so it can never print resets in -3m0s.
  • A 403 that still has quota remaining is left alone as a genuine permissions
    failure — TestForbiddenWithQuotaLeftIsNotRateLimit pins that.
  • 7 new tests, including one built from the exact headers observed in the report.

🤖 Generated with Claude Code

…ivate repo

GitHub answers an exhausted API quota with a plain 403 Forbidden, which is
indistinguishable from a permissions failure unless you read the response
headers. Both `self update` and install.sh assumed the permissions case for
every 403 and told the user to "set GITHUB_TOKEN if the repo is private" — so a
user on a PUBLIC repo who had merely used up the anonymous 60-requests/hour
quota went hunting for a permissions problem that did not exist, while the real
cause (and the fact that it clears itself in minutes) stayed hidden.

The distinguishing signal is X-RateLimit-Remaining: 0, plus Retry-After for
secondary limits. apiError now separates four cases:

  rate limited -> names the limit that was hit, says plainly that it is NOT a
                  permissions problem, gives the reset countdown, and still
                  offers GITHUB_TOKEN as the fix (it raises the cap to 5000/h)
  401          -> the token is set but was rejected
  404          -> keeps the private-repo hint, which is where it belongs
  other        -> the bare status

Before:
  Github API https://api.github.com/... returned 403 Forbidden (set GITHUB_TOKEN
  if the repo is private).

After:
  GitHub API rate limit exceeded (60 requests/hour), resets in 10m0s.
  This is not a permissions problem — the repository is reachable, you have
  simply used up the anonymous quota for your IP.
  Set GITHUB_TOKEN (or GH_TOKEN) to raise the limit to 5000 requests/hour: <url>

Applied to both the API path (selfupdate.go) and the asset-download path
(update.go), since the reported failure hit the asset download first.

install.sh cannot read the headers — its api() helper uses `curl -f`, which
discards the error body — so it probes /rate_limit (which does not count against
the quota) and appends the same clarification when the quota is spent.

The reset countdown is omitted rather than rendered when the stamp is in the
past or unparseable, so it can never print "resets in -3m0s".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant