fix(auth): retry Anthropic OAuth code exchange on HTTP 429 instead of burning the authorization code - #58014
Conversation
The Anthropic OAuth login exchanges the pasted authorization code at the token endpoint. Anthropic rate-limits that endpoint per IP; on a transient 429 the exchange previously failed straight through to the console fallback host (which 404s) and gave up — burning the single-use authorization code and forcing the user back through the whole browser round-trip, usually into the same rate limit. Retry 429s at the same endpoint with capped exponential backoff (max 4 attempts, honoring Retry-After, delay capped at 60s) before falling through to the next host. Non-429 errors keep the existing fail-fast fallback semantics. Mirrors the 429 handling in the Codex device-code login (_codex_device_code_login in hermes_cli/auth.py).
|
Thanks @aolater3 — your problem write-up was accurate (single-use code gets burned, and the fallback to The 429 at the token endpoint isn't transient rate-limiting — it's a permanent UA-prefix block. Anthropic 429s any
So retrying with the same |
What does this PR do?
Retries the Anthropic OAuth login token exchange on transient HTTP 429 with capped backoff (honoring
Retry-After, max 4 attempts per endpoint), instead of failing the flow and burning the user's single-use authorization code.Today,
run_hermes_oauth_login_pure()treats a 429 from the token endpoint like any other error: it falls through to the legacyconsole.anthropic.comhost (which 404s) and gives up withToken exchange failed: HTTP Error 429: Too Many Requests. Since the pasted authorization code is single-use, the user must redo the whole browser round-trip — from the same IP that was just rate-limited, so the retry usually 429s again. This can lock users out of Anthropic subscription login for an extended period.The fix mirrors the 429 handling that already exists in the Codex device-code login (
_codex_device_code_login()inhermes_cli/auth.py): retry in place with exponential backoff (2^attempt, honoringRetry-Afterwhen present, delay clamped to [1, 60]s, 4 attempts), then fall through to the next host in_OAUTH_TOKEN_URLS. Non-429 errors keep the exact existing fail-fast-to-next-endpoint semantics — verified by a dedicated test.Related Issue
Fixes #58013
Related: #12905 / #6475 (broader Anthropic OAuth/subscription pain), PR #46535 (same class of fix on the token refresh path in cron; this PR covers the login exchange path).
Type of Change
Changes Made
agent/anthropic_adapter.py— wrap the per-endpointurlopenin a bounded retry loop: 429 → backoff and retry (honoringRetry-After); any otherHTTPError/exception → log and move to the next endpoint exactly as before; success → break out of both loops.tests/agent/test_anthropic_oauth_pkce.py— three regression tests:test_login_token_exchange_retries_on_429— a single 429 withRetry-After: 3is retried at the same endpoint (not skipped to the fallback host) and sleeps exactly 3s.test_login_429_exhausts_retries_then_falls_back— persistent 429s exhaust 4 attempts (backoff 2s/4s/8s, no sleep after the final attempt) and then fall through to the console host, preserving endpoint-fallback semantics.test_login_non_429_http_error_is_not_retried— a 404 still fails fast to the next host with no retries and no sleeps.How to Test
pytest tests/agent/test_anthropic_oauth_pkce.py -v— 7 passed (4 pre-existing + 3 new).agent/anthropic_adapter.pyreverted tomain,test_login_token_exchange_retries_on_429andtest_login_429_exhausts_retries_then_falls_backfail;test_login_non_429_http_error_is_not_retriedpasses on both (it pins the preserved behavior).Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — ran the touched area instead:tests/agent/test_anthropic_oauth_pkce.py,tests/agent/test_anthropic_oauth_ua_prefix.py,tests/agent/test_anthropic_adapter.py,tests/hermes_cli/test_auth_commands.py(233 passed; 3 pre-existing environment-dependent failures inTestRunOauthSetupTokenthat reproduce identically on pristinemain— real local credentials leak into the mocks; they pass under the hermetic runner's blanked env). Full suite left to CI.Documentation & Housekeeping
docs/, docstrings) — N/A (behavioral fix, comments inline)cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/Aurllib/timeonly, no platform-specific codeScreenshots / Logs
Retry in action (previously an immediate
Token exchange failed):