Skip to content

Attempt OAuth token refresh on 429 with retry_after=0 (SMA-513)#321

Merged
botfarm-coder merged 2 commits intomainfrom
adobrushskiy/sma-513-429-oauth-refresh
Mar 21, 2026
Merged

Attempt OAuth token refresh on 429 with retry_after=0 (SMA-513)#321
botfarm-coder merged 2 commits intomainfrom
adobrushskiy/sma-513-429-oauth-refresh

Conversation

@botfarm-coder
Copy link
Collaborator

Summary

  • When the Anthropic usage API returns HTTP 429 with Retry-After: 0 (or absent), attempt an OAuth token refresh before falling back to backoff — this pattern indicates an expired token, not genuine rate limiting
  • If refresh produces a new token fingerprint, retry the API call immediately; if same fingerprint or refresh fails, fall through to existing backoff logic
  • Adds 6 test cases covering: successful refresh on retry_after=0, absent header, same-fingerprint fallthrough, refresh failure fallthrough, nonzero retry_after (no refresh), and retry-after-refresh failure

Test plan

  • 429 with retry_after=0 → refresh → new token → retry succeeds
  • 429 with absent Retry-After → refresh → new token → retry succeeds
  • 429 with retry_after=0 → refresh → same fingerprint → falls through to backoff
  • 429 with retry_after=0 → refresh returns None → falls through to backoff
  • 429 with retry_after>0 → no refresh attempted → normal backoff
  • 429 refresh → new token → retry also fails → backoff applied
  • Full test suite passes (3563 tests)

429 responses with Retry-After: 0 or absent header often indicate an
expired OAuth token rather than genuine rate limiting. Add token refresh
logic to the 429 handler: if refresh produces a new fingerprint, retry
the API call; otherwise fall through to normal backoff.
self._attempt_records = []
try:
data = self._fetch(refreshed_token)
except Exception:
Copy link
Collaborator

Choose a reason for hiding this comment

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

CODEX: This retry path catches every failure from the refreshed request and converts it into _handle_429(). If the refreshed token comes back with 401 (or any other non-429 HTTP error), we silently classify an auth failure as rate limiting, skip _handle_401(), and never trigger the existing auth-backoff/notification path. Please preserve the status-specific handling here instead of collapsing all retry failures into 429 backoff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — split the broad except Exception into except httpx.HTTPStatusError (routes 401s to _handle_401(), other HTTP errors to _handle_429()) and a separate except Exception for non-HTTP failures. Added a test for 401-after-refresh routing.

# ---------------------------------------------------------------------------


class Test429TokenRefresh:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: there's an existing _make_429_error(retry_after=...) helper (line ~1239) that wraps both the response and error construction. All other 429 tests in this file use it. These tests manually create the response + error — switch to _make_429_error for consistency.

Suggested change
class Test429TokenRefresh:
error = _make_429_error(retry_after="0")

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — switched all tests to use _make_429_error().

Copy link
Collaborator

@botfarm-reviewer botfarm-reviewer left a comment

Choose a reason for hiding this comment

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

Review iteration 1
Claude: changes_requested
Codex: changes_requested
Overall: changes_requested

- Replace broad `except Exception` in 429 refresh retry path with
  separate `httpx.HTTPStatusError` handler that routes 401s to
  `_handle_401()` instead of collapsing all failures into 429 backoff
- Switch Test429TokenRefresh tests to use `_make_429_error()` helper
  for consistency with other 429 tests
- Add test for 401-after-refresh routing to auth handler
Copy link
Collaborator

@botfarm-reviewer botfarm-reviewer left a comment

Choose a reason for hiding this comment

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

Clean implementation with comprehensive test coverage.

What I checked:

  • Logic flow: 429 with retry_after=0/absent → refresh → fingerprint check → retry is correct and follows the established pattern from the 401 handler
  • All error paths (refresh failure, same fingerprint, retry failure, retry-401, retry-other-error) are handled and fall through appropriately
  • Success path correctly resets both rate-limit and auth-failure state before parsing
  • parse_retry_after("0") returns 0 (int), so retry_after == 0 matches as expected; past HTTP-dates return 0.0 (float) which also matches via ==
  • Test coverage is thorough: 7 test cases covering success, fallthrough, and edge-case scenarios
  • Existing test fixtures correctly updated with refresh_token.return_value = None to prevent false-positive refresh attempts

Minor observations (not blocking):

  • The inner except httpx.HTTPStatusError catches all non-401 HTTP errors on retry (e.g. 500, 503) and routes them through _handle_429 — technically this increments the 429 counter for non-429 errors, but the effect is just conservative backoff, which is harmless
  • The fallback _handle_429 uses the original retry_after_header (0 or None), not the retry response's header — acceptable since the original header is what triggered the refresh path

@botfarm-coder botfarm-coder merged commit a1b92a5 into main Mar 21, 2026
2 checks passed
@botfarm-coder botfarm-coder deleted the adobrushskiy/sma-513-429-oauth-refresh branch March 21, 2026 00:10
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.

2 participants