{"record":{"id":"81f2393f7be3fcd2","repo":"plandex-ai/plandex","slug":"refresh-failed-status-d-s","errorCode":null,"errorMessage":"refresh failed - status %d: %s","messagePattern":"refresh failed - status (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/cli/lib/claude_max.go","lineNumber":318,"sourceCode":"\treq, err := http.NewRequest(\"POST\", claudeMaxTokenUrl, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - create request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"anthropic-beta\", shared.AnthropicClaudeMaxBetaHeader)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - http: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, 0, fmt.Errorf(\"refresh failed - read body: %w\", err)\n\t\t}\n\t\treturn nil, resp.StatusCode, fmt.Errorf(\"refresh failed - status %d: %s\", resp.StatusCode, b)\n\t}\n\n\tvar r types.OauthResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&r); err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - decode: %w\", err)\n\t}\n\n\tnewCreds := &types.OauthCreds{\n\t\tOauthResponse: r,\n\t\tExpiresAt:     time.Now().Add(time.Duration(r.ExpiresIn) * time.Second),\n\t}\n\n\t// persist updated creds\n\taccountCreds.ClaudeMax = newCreds\n\tif err := SetAccountCredentials(accountCreds); err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - save: %w\", err)\n\t}\n","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/cli/lib/claude_max.go#L300-L336","documentation":"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.","triggerScenarios":"The POST to claudeMaxTokenUrl completes but resp.StatusCode != http.StatusOK — expired or revoked refresh token, bad client credentials, server 5xx, or rate limiting.","commonSituations":"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.","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)."],"exampleFix":"// before\ncreds, _, err := refreshCreds(accountCreds)\nif err != nil { return err }\n// after\ncreds, status, err := refreshCreds(accountCreds)\nif err != nil {\n    if status == http.StatusUnauthorized || status == http.StatusBadRequest {\n        // refresh token expired/revoked: force re-login\n        _ = ClearAccountCredentials()\n        return startOAuthLoginFlow(ctx)\n    }\n    return retry.WithBackoff(func() error { _, _, err = refreshCreds(accountCreds); return err }, 3)\n}","handlingStrategy":"fallback","validationCode":"// proactively refresh before expiry so expired refresh tokens are caught early\nif creds.ExpiresAt.Before(time.Now().Add(5 * time.Minute)) {\n    return refreshClaudeMaxCredsIfNeeded()\n}","typeGuard":"func isAuthRejection(status int) bool {\n    return status == http.StatusUnauthorized || status == http.StatusBadRequest\n}","tryCatchPattern":"creds, status, err := refreshCreds(acct)\nif err != nil {\n    if isAuthRejection(status) {\n        return reAuthInteractive(ctx) // refresh token dead: full login\n    }\n    return retryWithBackoff(func() error { _, _, err = refreshCreds(acct); return err }, 3)\n}","preventionTips":["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."],"tags":["http","oauth","http-status","token-expired","authentication"],"backgroundTag":"oauth-refresh-token-rejected","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}