fix(auth): use correct OAuth endpoint, parse Retry-After, add backoff guard#40
Open
iceteaSA wants to merge 7 commits into
Open
fix(auth): use correct OAuth endpoint, parse Retry-After, add backoff guard#40iceteaSA wants to merge 7 commits into
iceteaSA wants to merge 7 commits into
Conversation
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
3200ef9 to
d39a1bf
Compare
…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.
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three issues with OAuth token refresh:
Wrong endpoint: Token refresh was hitting
api.anthropic.com/v1/oauth/token(the Workload Identity Federation endpoint) instead ofplatform.claude.com/v1/oauth/token(the Claude Code OAuth endpoint). The authorize URL was alreadyplatform.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.Retry-After ignored: 429 responses include a
Retry-Afterheader that was ignored. Backoff used a flat exponential starting at 5 minutes regardless of the server's requested delay.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_URLfromapi.anthropic.com/v1/oauth/tokentoplatform.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
ClaudeOAuthRefreshErrornow captures theRetry-Afterheader from 429 responses. Supports both seconds (60) and HTTP date formats.buildRefreshOperationErroruses 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
expiredAgoMs)expiresInMsin background timer backoff skip logChanges
packages/core/src/constants.tsTOKEN_URLtoplatform.claude.compackages/core/src/auth.tsretryAftertoClaudeOAuthRefreshError, parse headerpackages/core/src/accounts.tsretryAfterin backoff calculationpackages/opencode/src/index.tspackages/opencode/src/tests/*.ts