diff --git a/internal/auth/codex/openai_auth.go b/internal/auth/codex/openai_auth.go index c273acae..64bc00a6 100644 --- a/internal/auth/codex/openai_auth.go +++ b/internal/auth/codex/openai_auth.go @@ -276,6 +276,10 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str if err == nil { return tokenData, nil } + if isNonRetryableRefreshErr(err) { + log.Warnf("Token refresh attempt %d failed with non-retryable error: %v", attempt+1, err) + return nil, err + } lastErr = err log.Warnf("Token refresh attempt %d failed: %v", attempt+1, err) @@ -284,6 +288,14 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr) } +func isNonRetryableRefreshErr(err error) bool { + if err == nil { + return false + } + raw := strings.ToLower(err.Error()) + return strings.Contains(raw, "refresh_token_reused") +} + // UpdateTokenStorage updates an existing CodexTokenStorage with new token data. // This is typically called after a successful token refresh to persist the new credentials. func (o *CodexAuth) UpdateTokenStorage(storage *CodexTokenStorage, tokenData *CodexTokenData) { diff --git a/internal/auth/codex/openai_auth_test.go b/internal/auth/codex/openai_auth_test.go new file mode 100644 index 00000000..3327eb4a --- /dev/null +++ b/internal/auth/codex/openai_auth_test.go @@ -0,0 +1,44 @@ +package codex + +import ( + "context" + "io" + "net/http" + "strings" + "sync/atomic" + "testing" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestRefreshTokensWithRetry_NonRetryableOnlyAttemptsOnce(t *testing.T) { + var calls int32 + auth := &CodexAuth{ + httpClient: &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + atomic.AddInt32(&calls, 1) + return &http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(strings.NewReader(`{"error":"invalid_grant","code":"refresh_token_reused"}`)), + Header: make(http.Header), + Request: req, + }, nil + }), + }, + } + + _, err := auth.RefreshTokensWithRetry(context.Background(), "dummy_refresh_token", 3) + if err == nil { + t.Fatalf("expected error for non-retryable refresh failure") + } + if !strings.Contains(strings.ToLower(err.Error()), "refresh_token_reused") { + t.Fatalf("expected refresh_token_reused in error, got: %v", err) + } + if got := atomic.LoadInt32(&calls); got != 1 { + t.Fatalf("expected 1 refresh attempt, got %d", got) + } +}