plandex-ai/plandex · error
refresh failed - status %d: %s
Error message
refresh failed - status %d: %s
What it means
refreshCreds reports the HTTP status returned by the Claude Max OAuth token endpoint when it is anything other than 200 OK, including the first 64KB of the response body. The wrapped status and body are the server's own diagnosis — e.g. invalid_grant for an expired/revoked refresh token, or 401/403/5xx for server-side issues.
Source
Thrown at app/cli/lib/claude_max.go:318
req, err := http.NewRequest("POST", claudeMaxTokenUrl, bytes.NewReader(body))
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-beta", shared.AnthropicClaudeMaxBetaHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - http: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - read body: %w", err)
}
return nil, resp.StatusCode, fmt.Errorf("refresh failed - status %d: %s", resp.StatusCode, b)
}
var r types.OauthResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, 0, fmt.Errorf("refresh failed - decode: %w", err)
}
newCreds := &types.OauthCreds{
OauthResponse: r,
ExpiresAt: time.Now().Add(time.Duration(r.ExpiresIn) * time.Second),
}
// persist updated creds
accountCreds.ClaudeMax = newCreds
if err := SetAccountCredentials(accountCreds); err != nil {
return nil, 0, fmt.Errorf("refresh failed - save: %w", err)
}
View on GitHub (pinned to e2d772072e)
Solutions
- Read the status and body in the error: 401/400 with invalid_grant means the refresh token is dead — re-run the OAuth login flow to obtain fresh credentials.
- Delete the stale stored credentials (SetAccountCredentials path) so the app falls back to interactive login instead of retrying a dead token.
- For 5xx, wait and retry with backoff; it is a server-side incident.
- For 429, respect the rate limit and back off before the next refresh.
- Verify the system clock is accurate (large skew can invalidate token requests).
Example fix
// before
creds, _, err := refreshCreds(accountCreds)
if err != nil { return err }
// after
creds, status, err := refreshCreds(accountCreds)
if err != nil {
if status == http.StatusUnauthorized || status == http.StatusBadRequest {
// refresh token expired/revoked: force re-login
_ = ClearAccountCredentials()
return startOAuthLoginFlow(ctx)
}
return retry.WithBackoff(func() error { _, _, err = refreshCreds(accountCreds); return err }, 3)
} Defensive patterns
Strategy: fallback
Validate before calling
// proactively refresh before expiry so expired refresh tokens are caught early
if creds.ExpiresAt.Before(time.Now().Add(5 * time.Minute)) {
return refreshClaudeMaxCredsIfNeeded()
} Type guard
func isAuthRejection(status int) bool {
return status == http.StatusUnauthorized || status == http.StatusBadRequest
} Try / catch
creds, status, err := refreshCreds(acct)
if err != nil {
if isAuthRejection(status) {
return reAuthInteractive(ctx) // refresh token dead: full login
}
return retryWithBackoff(func() error { _, _, err = refreshCreds(acct); return err }, 3)
} Prevention
- Refresh tokens proactively before expiry rather than lazily on use.
- Detect invalid_grant / 401 responses and trigger re-login instead of endless retries.
- Persist the returned status alongside the error for callers to branch on.
- Keep system clocks synchronized (NTP) to avoid token validity issues.
When it happens
Trigger: The POST to claudeMaxTokenUrl completes but resp.StatusCode != http.StatusOK — expired or revoked refresh token, bad client credentials, server 5xx, or rate limiting.
Common situations: Long-lived sessions where the refresh token expired or was revoked (user logged out elsewhere / password change), API incidents returning 5xx, rate limits after many concurrent refreshes, or clock skew invalidating token validity windows.
Related errors
- token exchange failed - error creating request: %s
- token exchange failed - error reading body: %s
- token exchange failed - status: %d, body: %s
- refresh failed - create request: %w
- refresh failed - http: %w
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/81f2393f7be3fcd2.
Report an issue: GitHub.