{"record":{"id":"65edfc3ad2f63510","repo":"chatboxai/chatbox","slug":"token-refresh-failed-error","errorCode":null,"errorMessage":"Token refresh failed: ${error}","messagePattern":"Token refresh failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/oauth/providers/anthropic.ts","lineNumber":154,"sourceCode":"  async refreshToken(credentials) {\n    if (!credentials.refreshToken) {\n      throw new Error('No refresh token available')\n    }\n\n    const response = await fetch(TOKEN_URL, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        grant_type: 'refresh_token',\n        client_id: CLIENT_ID,\n        refresh_token: credentials.refreshToken,\n      }),\n    })\n\n    if (!response.ok) {\n      const error = await response.text()\n      log.error('[OAuth:Anthropic] Token refresh failed:', error)\n      throw new Error(`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      accessToken: data.access_token,\n      refreshToken: data.refresh_token,\n      expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n    }\n  },\n}\n","sourceCodeStart":136,"sourceCodeEnd":170,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/oauth/providers/anthropic.ts#L136-L170","documentation":"Thrown when the POST to Anthropic's TOKEN_URL with grant_type=refresh_token returns non-2xx. The raw error body is interpolated, so Anthropic's reason (typically invalid_grant when the refresh token is revoked or expired) appears in the message. This terminates the refresh attempt and propagates to the caller.","triggerScenarios":"The refresh token was revoked by the user (e.g. they disconnected the app from their Anthropic account); the token expired due to inactivity; CLIENT_ID changed; concurrent refresh calls invalidated the previous token (rotation).","commonSituations":"Long-idle user returns after Anthropic revoked the grant; user manually revoked access in their Anthropic console; a second device refreshed first and the old refresh token was rotated; CLIENT_ID constant drifted.","solutions":["Read the interpolated body — invalid_grant means the refresh token is dead and a fresh interactive login is required.","On invalid_grant, clear stored credentials and prompt startLogin() rather than retrying the same dead token.","For transient 5xx or network errors, retry once with backoff before giving up.","Serialize refresh calls per-account to avoid token-rotation races."],"exampleFix":"// before\nconst creds = await provider.refreshToken(stored)\n\n// after\ntry {\n  const creds = await provider.refreshToken(stored)\n  return creds\n} catch (e) {\n  if (/invalid_grant|refresh token/i.test(String(e))) {\n    await clearStoredCredentials()\n    return startInteractiveLogin()\n  }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await provider.refreshToken(credentials)\n} catch (e) {\n  const msg = String(e)\n  if (/invalid_grant|revok|expired/i.test(msg)) {\n  // refresh token is dead — clear and force interactive re-login\n  await clearStoredCredentials()\n  throw new ReauthRequiredError(msg)\n  }\n  if (/5\\d\\d|network|fetch/i.test(msg)) return await provider.refreshToken(credentials) // single retry\n  throw e\n}","preventionTips":["Serialize refresh calls per account to avoid token-rotation races.","Refresh proactively before expiry (the 5-minute skew buffer helps) rather than reactively.","Clear stored credentials on any invalid_grant to avoid hammering a dead token."],"tags":["oauth","network","anthropic","refresh-token"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}