Skip to content

fix(credentials): recheck expiry after advisory refresh failure - #2058

Open
Linxiushen wants to merge 2 commits into
anthropics:mainfrom
Linxiushen:codex/token-cache-expired-fallback
Open

Linxiushen wants to merge 2 commits into
anthropics:mainfrom
Linxiushen:codex/token-cache-expired-fallback

Conversation

@Linxiushen

Copy link
Copy Markdown

An advisory credential refresh can fail after the previously cached bearer has expired. TokenCache currently decides whether fallback is allowed before making the refresh attempt, then returns that bearer without rechecking its expiry after the failure. A Messages request can therefore carry an expired bearer while the original refresh error is swallowed.

Recheck the fallback token's expiry when the failed refresh finishes. If it has expired, preserve the provider's original exception. A still-valid token keeps the existing advisory fallback behavior, and a later successful refresh recovers normally. This does not change in-flight invalidation semantics.

The public-client regression covers the built-in WorkloadIdentityCredentials path: a cached token has 31 seconds remaining, the SDK retries a token-endpoint 401 once, and the retry returns 503 after expiry. respx advances the credential clock by 16 seconds for each exchange. Each modeled exchange is below the default 30-second token timeout; their combined 32 seconds exceed the remaining 31-second validity. This is deterministic offline coverage, not a measured network latency or timeout test. Sync and async Messages clients must propagate the original 503, send no second Messages request, and recover with a fresh bearer on the following call. Additional public-client controls cover a failure just before, exactly at, and just after expiry.

Validation on Windows / Python 3.10.20:

  • Applying the identical regression tests to unchanged base 4c7b598466b88559583f98216139be8e9b34c1fc produced 6 failed / 2 passed cases. Both built-in WIF cases swallowed the original failure and sent the expired bearer.
  • scripts/test tests/lib/test_credentials.py -k 'TestTokenCache or advisory_failure_checks_expiry_before_sending or failed_workload_retry_does_not_send_expired_bearer or advisory_failure_serves_cached_and_mandatory_failure_raises or cancelled_leader_releases_waiters or concurrent_cold_calls_share_one_provider_call' -n0: 43 passed / 2 skipped in each of the Pydantic 2 and Pydantic 1 matrices. The two existing os.fork tests do not apply on Windows.
  • scripts/format passed. scripts/lint passed Ruff and dependency-cap checks; full lint remains red because Pyright reports 229 errors / 19 warnings on both the unchanged base and this patch. All 248 diagnostic blocks, including continuations, match after normalizing checkout paths and line/column offsets; none concern the two changed files. The subsequent import-lint stage was not reached.

Release note: avoid sending an expired cached bearer after an advisory credential refresh fails.

Cross-SDK review: the current TypeScript, Go, Java, C#, and PHP native cache implementations do not share this waited-failure fallback path. Their advisory paths return immediately or their blocking refresh paths propagate the provider error. Ruby has the analogous blocking fallback; the sibling port is anthropics/anthropic-sdk-ruby#276.

Prepared with AI assistance. No live Anthropic API, model, GPU, or production performance validation was used.

This branch has not been deployed

No deployments
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