Skip to content

Distinguish GitHub rate limiting from an invalid token - #107

Merged
rongxin-liu merged 3 commits into
developfrom
fix/rate-limit-vs-invalid-token
Aug 21, 2026
Merged

Distinguish GitHub rate limiting from an invalid token#107
rongxin-liu merged 3 commits into
developfrom
fix/rate-limit-vs-invalid-token

Conversation

@rongxin-liu

@rongxin-liu rongxin-liu commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Problem

_validate_github_token() treated 401 and 403 as the same failure:

if response.status_code in (401, 403):
    raise InvalidTokenError()

GitHub returns 403 for an exhausted rate limit as well as for a revoked token, so a student whose hourly API budget is spent is told their token is broken, sent to https://cs50.dev/restart, and has their cached credentials discarded by logout().

That advice makes things worse. /restart is the most API-expensive route on cs50.dev, so each attempt spends more of an already-empty budget — and because GitHub counts rejected requests too, retrying inflates usage further. The workaround only appears to work because revoking, re-authorizing, and waiting for a codespace restart takes long enough for the hourly window to roll over, which is why students report repeating it before every submission.

This is not rare. Production telemetry over one 26-minute window showed 30 students blocked, 16 of them at 2x or worse against a 5,000/hour limit, peaking at 39,558 requests used.

Changes

1. A 403 with an exhausted rate limit now raises RateLimitError.

Response Before After
403, rate limit exhausted "issue authenticating… visit /restart" + logout() "You have reached GitHub's hourly API rate limit. Please try again after 13:24."
403, budget remaining same message unchanged
401 same message unchanged

The retry time comes from x-ratelimit-reset, falling back to a message without a time when that header is missing or unparseable. Crucially this path does not call logout() — the credential is valid, and discarding it forces an unnecessary re-authentication.

2. Both messages now identify the student, since these failures are reported by screenshot:

You have reached GitHub's hourly API rate limit. Please try again after 13:24.
Details: user student50, id 123456789, request CBE0:3611EE:594CD9:63507E:6A877880

The login comes from CS50_GH_USER, the numeric id is parsed from GitHub's own "API rate limit exceeded for user ID …" message (the only place it appears on a failed request), and the request id from x-github-request-id for escalating to GitHub. The id falls back to RepositoryName, which in a CS50 codespace is the student's numeric id. Each field is omitted when unavailable, so no bare Details: line appears, and the fallback is guarded with .isdigit() so local development does not print a project name as an id.

3. Version bumped to 3.2.2.

Tests

Suite goes 73 -> 79, all passing. test_forbidden_token_403 now sets an explicit x-ratelimit-remaining header so it still asserts InvalidTokenError for a genuine authorization failure. New tests cover the exhausted case, the secondary retry-after case, reset-time formatting with both fallbacks, identity capture, and _identify() returning empty rather than a partial line.

Note

This reaches students only through the codespace image, so it needs a rebuild after release.

A 403 with an exhausted rate limit now raises RateLimitError instead of InvalidTokenError, so a throttled student is told to wait rather than to restart their codespace.
@rongxin-liu rongxin-liu self-assigned this Aug 21, 2026
Students report these failures by screenshot, so the message carries their GitHub login, numeric id, and the GitHub request id.
@rongxin-liu
rongxin-liu merged commit c01e2e2 into develop Aug 21, 2026
1 check passed
@rongxin-liu
rongxin-liu deleted the fix/rate-limit-vs-invalid-token branch August 21, 2026 12:06
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