Skip to content

fix(auth): retry Anthropic OAuth code exchange on HTTP 429 instead of burning the authorization code - #58014

Closed
aolater3 wants to merge 1 commit into
NousResearch:mainfrom
aolater3:fix/anthropic-oauth-429-retry
Closed

fix(auth): retry Anthropic OAuth code exchange on HTTP 429 instead of burning the authorization code#58014
aolater3 wants to merge 1 commit into
NousResearch:mainfrom
aolater3:fix/anthropic-oauth-429-retry

Conversation

@aolater3

@aolater3 aolater3 commented Jul 4, 2026

Copy link
Copy Markdown

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 legacy console.anthropic.com host (which 404s) and gives up with Token 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() in hermes_cli/auth.py): retry in place with exponential backoff (2^attempt, honoring Retry-After when 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

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/anthropic_adapter.py — wrap the per-endpoint urlopen in a bounded retry loop: 429 → backoff and retry (honoring Retry-After); any other HTTPError/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 with Retry-After: 3 is 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

  1. pytest tests/agent/test_anthropic_oauth_pkce.py -v — 7 passed (4 pre-existing + 3 new).
  2. Proof the new tests guard the fix: with agent/anthropic_adapter.py reverted to main, test_login_token_exchange_retries_on_429 and test_login_429_exhausts_retries_then_falls_back fail; test_login_non_429_http_error_is_not_retried passes on both (it pins the preserved behavior).
  3. Real-world check: this patch has been running on my local v0.18.0 install; a login that previously died on the first 429 completes after the backoff retry.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate (closest is fix(cron): fallback providers when OAuth token refresh returns 429 #46535, which covers the refresh path, not the login exchange)
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and 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 in TestRunOauthSetupToken that reproduce identically on pristine main — real local credentials leak into the mocks; they pass under the hermetic runner's blanked env). Full suite left to CI.
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26 (Apple Silicon), Python 3.11.15

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A (behavioral fix, comments inline)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — stdlib urllib/time only, no platform-specific code
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

Retry in action (previously an immediate Token exchange failed):

Anthropic is rate-limiting login requests (429); retrying in 3s...
$ pytest tests/agent/test_anthropic_oauth_pkce.py -v
...
tests/agent/test_anthropic_oauth_pkce.py::test_authorization_url_state_is_not_pkce_verifier PASSED
tests/agent/test_anthropic_oauth_pkce.py::test_login_token_exchange_uses_platform_claude_host PASSED
tests/agent/test_anthropic_oauth_pkce.py::test_login_token_exchange_falls_back_to_console_host PASSED
tests/agent/test_anthropic_oauth_pkce.py::test_callback_state_mismatch_aborts PASSED
tests/agent/test_anthropic_oauth_pkce.py::test_login_token_exchange_retries_on_429 PASSED
tests/agent/test_anthropic_oauth_pkce.py::test_login_429_exhausts_retries_then_falls_back PASSED
tests/agent/test_anthropic_oauth_pkce.py::test_login_non_429_http_error_is_not_retried PASSED

============================== 7 passed in 2.24s ===============================

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).
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists labels Jul 4, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Thanks @aolater3 — your problem write-up was accurate (single-use code gets burned, and the fallback to console.anthropic.com 404s). But we're closing this in favor of a root-cause fix merged via #58178.

The 429 at the token endpoint isn't transient rate-limiting — it's a permanent UA-prefix block. Anthropic 429s any /v1/oauth/token request whose User-Agent starts with claude-code/ (the anti-abuse net for Max-sub-as-API-key). Verified live against platform.claude.com:

User-Agent Result
claude-code/2.1.200 (external, cli) 429 rate_limit (every time)
axios/1.7.9 400 invalid_grant (reached validation)

So retrying with the same claude-code/ UA would 429 on every attempt and still burn the code — the backoff never clears because the discriminator is the UA, not request volume. The fix is to stop sending claude-code/ on the token endpoint (use axios/, matching the real Claude Code CLI's exchange client), which #58178 does at all three token-endpoint call sites while keeping claude-code/ on the inference path.

Fixed in #58178: #58178

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Anthropic OAuth login: transient HTTP 429 on token exchange burns the single-use authorization code (no retry)

3 participants