{"record":{"id":"a35ada957091e8af","repo":"mastra-ai/mastra","slug":"anthropic-token-refresh-failed-error","errorCode":null,"errorMessage":"Anthropic token refresh failed: ${error}","messagePattern":"Anthropic token refresh failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/sdk/src/auth/providers/anthropic.ts","lineNumber":145,"sourceCode":"\n/**\n * Refresh Anthropic OAuth token\n */\nexport async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {\n  const response = await fetch(TOKEN_URL, {\n    method: 'POST',\n    signal: AbortSignal.timeout(15_000),\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      grant_type: 'refresh_token',\n      client_id: CLIENT_ID,\n      refresh_token: refreshToken,\n    }),\n  });\n\n  if (!response.ok) {\n    const error = await response.text();\n    throw new Error(`Anthropic token refresh failed: ${error}`);\n  }\n\n  const data = (await response.json()) as {\n    access_token: string;\n    refresh_token: string;\n    expires_in: number;\n  };\n\n  return {\n    refresh: data.refresh_token,\n    access: data.access_token,\n    expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n  };\n}\n\nexport const anthropicOAuthProvider: OAuthProviderInterface = {\n  id: 'anthropic',\n  name: 'Anthropic (Claude Pro/Max)',","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/auth/providers/anthropic.ts#L127-L163","documentation":"`refreshAnthropicToken` POSTs to Anthropic's token endpoint with grant_type=refresh_token to obtain a new access token. When the response is not ok it throws 'Anthropic token refresh failed: <response body>', embedding the upstream error (typically invalid_grant for a revoked/expired/rotated refresh token). Callers (e.g. anthropicOAuthProvider.refreshToken) surface this whenever stored credentials can no longer be renewed.","triggerScenarios":"The stored refresh token was revoked (user logged out / reset Claude credentials), already rotated by another client or process (single-use refresh tokens), or expired; Anthropic returns 4xx for a malformed/unknown token; a 5xx or network outage also lands here since any non-ok status throws.","commonSituations":"Credentials persisted in a database/config then invalidated server-side; two machines (or dev and CI) sharing the same account and racing refreshes, invalidating each other's tokens; clock or storage corruption making a stale token look valid; temporary Anthropic outage causing 5xx on an otherwise valid token.","solutions":["If the body says invalid_grant, the refresh token is dead: trigger a full re-login (startAnthropicLogin + completeAnthropicLogin) and replace the stored credentials.","Check for concurrent refreshes — if refresh tokens rotate on use, ensure only one process refreshes at a time (e.g. lock or single refresher) and persist the rotated token immediately.","For 5xx/network errors, retry with backoff; the token itself may still be valid.","Verify the stored refresh value is the current one and was not truncated or overwritten by a partial credential save.","Inspect the embedded error text — it distinguishes auth problems (re-auth needed) from transient server problems (retry)."],"exampleFix":"// before\nconst creds = await refreshAnthropicToken(stored.refresh); // throws on revoked token, crash loop\n// after\nlet creds;\ntry {\n  creds = await refreshAnthropicToken(stored.refresh);\n} catch (e) {\n  if (/invalid_grant/i.test(String(e))) {\n    creds = await runInteractiveLogin(); // full re-auth; refresh token is unrecoverable\n  } else {\n    throw e; // transient: retry with backoff\n  }\n}","handlingStrategy":"retry","validationCode":"// skip refresh when the access token is still valid (5-min buffer already baked in)\nif (creds.expires > Date.now() + 60_000) {\n  return creds; // no refresh needed\n}\nif (!creds.refresh) throw new Error('No refresh token stored — full re-login required');","typeGuard":"function isRefreshFailed(e: unknown): e is Error & { message: string } {\n  return e instanceof Error && e.message.startsWith('Anthropic token refresh failed:');\n}","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return await refreshAnthropicToken(refreshToken);\n  } catch (e) {\n    if (isRefreshFailed(e) && /invalid_grant/i.test(e.message)) {\n      // refresh token revoked/rotated: unrecoverable — force full re-login\n      return forceReauthentication();\n    }\n    if (attempt === 2) throw e; // transient after backoff: give up\n    await sleep(2 ** attempt * 500); // 5xx/network: exponential backoff\n  }\n}","preventionTips":["Refresh proactively before expiry so you never refresh with a token that may have been revoked mid-session.","Persist rotated refresh tokens immediately — they are often single-use; losing the rotation breaks the chain.","Coordinate refreshes across processes/machines (lock or central refresher) to avoid racing rotations.","Distinguish invalid_grant (re-auth needed) from 5xx (retry) by inspecting the embedded error text.","Never log refresh or access tokens when recording the failure body."],"tags":["oauth","anthropic","token-refresh","http-error","credentials"],"backgroundTag":"oauth-token-refresh-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}