Skip to content

fix(auth): use correct OAuth endpoint, parse Retry-After, add backoff guard#40

Open
iceteaSA wants to merge 7 commits into
cortexkit:mainfrom
iceteaSA:fix/oauth-refresh-429
Open

fix(auth): use correct OAuth endpoint, parse Retry-After, add backoff guard#40
iceteaSA wants to merge 7 commits into
cortexkit:mainfrom
iceteaSA:fix/oauth-refresh-429

Conversation

@iceteaSA
Copy link
Copy Markdown
Contributor

@iceteaSA iceteaSA commented May 22, 2026

Problem

Three issues with OAuth token refresh:

  1. Wrong endpoint: Token refresh was hitting api.anthropic.com/v1/oauth/token (the Workload Identity Federation endpoint) instead of platform.claude.com/v1/oauth/token (the Claude Code OAuth endpoint). The authorize URL was already platform.claude.com — the token URL was the only inconsistency. This mismatch caused 429 rate limits because Claude Code PKCE refresh tokens are not intended for the WIF endpoint.

  2. Retry-After ignored: 429 responses include a Retry-After header that was ignored. Backoff used a flat exponential starting at 5 minutes regardless of the server's requested delay.

  3. Noisy per-request retries: When the token is expired and backoff is active, every request entered refreshMainAccessToken(), checked backoff internally, threw, and logged — generating noisy per-request log entries.

Fix

1. Use correct token endpoint (platform.claude.com)

Changed TOKEN_URL from api.anthropic.com/v1/oauth/token to platform.claude.com/v1/oauth/token. This is the dedicated OAuth service that Claude Code binary, ex-machina plugin, and OpenClaw plugin all use successfully without 429s.

2. Retry-After header support

ClaudeOAuthRefreshError now captures the Retry-After header from 429 responses. Supports both seconds (60) and HTTP date formats. buildRefreshOperationError uses it as the backoff duration when available, falling back to exponential backoff otherwise.

3. Request-path backoff guard

Checks backoff before entering refreshMainAccessToken() on the request path. When backoff is active, throws immediately with a clean log entry — no lease check, no file lock, no redundant log noise.

4. Debug logging improvements

  • Logs new token expiry after successful refresh (was logging stale pre-refresh value)
  • Logs how long ago the token expired on request-path refresh (expiredAgoMs)
  • Includes expiresInMs in background timer backoff skip log

Changes

File Change
packages/core/src/constants.ts Fix TOKEN_URL to platform.claude.com
packages/core/src/auth.ts Add retryAfter to ClaudeOAuthRefreshError, parse header
packages/core/src/accounts.ts Use retryAfter in backoff calculation
packages/opencode/src/index.ts Request-path backoff guard, improved debug logging
packages/opencode/src/tests/*.ts Updated tests

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 6 files

Re-trigger cubic

iceteaSA added 3 commits May 23, 2026 07:53
ClaudeOAuthRefreshError now captures the Retry-After header from 429
responses. Supports both seconds and HTTP date formats. buildRefreshOperationError
uses it as the backoff duration when available, falling back to exponential
backoff otherwise.
Skip refreshMainAccessToken() entirely when backoff is active on the
request path. Previously each request entered the function, checked
backoff internally, and threw — generating noisy per-request log entries.
Now the guard runs before entering the function, producing a single
clean log entry and throw.
- Log new token expiry after successful refresh (was logging stale value)
- Log how long ago token expired on request-path refresh
- Include expiresInMs in background timer backoff skip log
@iceteaSA iceteaSA force-pushed the fix/oauth-refresh-429 branch from 3200ef9 to d39a1bf Compare May 23, 2026 05:55
@iceteaSA iceteaSA changed the title fix(core): reduce OAuth refresh frequency and honor Retry-After fix(auth): parse Retry-After header and add request-path backoff guard May 23, 2026
…ropic.com

The OAuth token refresh was hitting api.anthropic.com/v1/oauth/token which
is the Workload Identity Federation (WIF) endpoint, not the Claude Code
OAuth endpoint. This caused 429 rate limits because Claude Code PKCE
refresh tokens are not intended for the WIF endpoint.

Changed to platform.claude.com/v1/oauth/token which is the dedicated
OAuth service that Claude Code binary and other working plugins use.
The authorize URL was already platform.claude.com — the token URL was
the only inconsistency.
@iceteaSA iceteaSA changed the title fix(auth): parse Retry-After header and add request-path backoff guard fix(auth): use correct OAuth endpoint, parse Retry-After, add backoff guard May 23, 2026
iceteaSA added 3 commits May 23, 2026 15:42
clearStaleMainRefreshError was clearing the backoff whenever the token
hash changed (assuming a new token = fresh start). With multiple opencode
sessions, process A refreshes (new token hash), process B sees the hash
change, clears its backoff, and immediately retries — hitting the rate
limit again. Now the backoff is preserved if nextRetryAt is still in the
future, regardless of token rotation.
Cloudflare's bot detection on platform.claude.com blocks requests
without a User-Agent header (Error 1010, returned as 403). This was
the root cause of all 429 refresh failures — the 403 was wrapped as
a rate_limit_error by the backend.

Sends User-Agent: claude-code/{VERSION} matching what Claude Code
binary and other working plugins use.
Anthropic's platform.claude.com rate-limits requests with User-Agent
'claude-code/*' but allows 'axios/1.13.6'. This matches the User-Agent
used by working plugins (openclaw).
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