{"record":{"id":"467f6b7f229e3076","repo":"chatboxai/chatbox","slug":"failed-to-refresh-oauth-credential-for-provideri","errorCode":null,"errorMessage":"Failed to refresh OAuth credential for ${providerId}","messagePattern":"Failed to refresh OAuth credential for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/renderer/adapters/index.ts","lineNumber":149,"sourceCode":"  const maybeDesktopPlatform = platform as unknown as { ipc?: OAuthIpcInvoker }\n  if (!maybeDesktopPlatform.ipc) {\n    throw new Error('OAuth IPC is only available on desktop')\n  }\n  return maybeDesktopPlatform.ipc\n}\n\nfunction createDesktopOAuthAdapter(oauthIpc?: OAuthIpcInvoker): OAuthAdapter {\n  return {\n    async refreshCredential(providerId: string, credential: OAuthCredentials): Promise<OAuthCredentials> {\n      const ipc = oauthIpc ?? getDefaultOAuthIpc()\n      const resultJson = await ipc.invoke(OAuthIpcChannels.REFRESH, providerId, JSON.stringify(credential))\n      const result = JSON.parse(resultJson) as {\n        success: boolean\n        credentials?: OAuthCredentials\n        error?: string\n      }\n      if (!result.success || !result.credentials) {\n        throw new Error(result.error || `Failed to refresh OAuth credential for ${providerId}`)\n      }\n      return result.credentials\n    },\n    persistCredential(providerId: string, credential: OAuthCredentials): void {\n      const settingsProviderId = toOAuthSettingsProviderId(providerId) || providerId\n      settingsStore.setState((currentSettings) => ({\n        providers: {\n          ...(currentSettings.providers || {}),\n          [settingsProviderId]: {\n            ...(currentSettings.providers?.[settingsProviderId] || {}),\n            oauth: credential,\n          },\n        },\n      }))\n    },\n    clearCredential(providerId: string): void {\n      const settingsProviderId = toOAuthSettingsProviderId(providerId) || providerId\n      settingsStore.setState((currentSettings) => {","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/renderer/adapters/index.ts#L131-L167","documentation":"Thrown by the desktop OAuth adapter's refreshCredential when the main-process refresh handler returns a result with success: false or missing credentials. The error message prefers the main process's error string, falling back to a generic per-provider message. This is the user-visible failure for token refresh (expired refresh token, revoked grant, network error during refresh).","triggerScenarios":"ipc.invoke(OAuthIpcChannels.REFRESH, providerId, credential) returns JSON whose success is false — the provider's token endpoint rejected the refresh_token (expired, revoked, invalid_grant), or the main process could not reach the endpoint. The result object carries an error string that becomes the thrown message.","commonSituations":"User revoked app access in the provider's account settings; refresh_token expired after long inactivity; the OAuth client secret changed server-side; the provider's token endpoint is temporarily unreachable. On Google/Anthropic providers, invalid_grant is the most common payload.","solutions":["Prompt the user to re-authenticate (start a fresh OAuth flow) since the refresh token is no longer usable.","Inspect result.error from the main process for invalid_grant or token_expired to decide between re-auth and retry.","If the failure is transient (network), retry once with backoff before forcing re-auth."],"exampleFix":"// before\nif (!result.success || !result.credentials) {\n  throw new Error(result.error || `Failed to refresh OAuth credential for ${providerId}`)\n}\n\n// after — surface a typed error so the UI can distinguish re-auth from retry\nif (!result.success || !result.credentials) {\n  const reason = result.error || 'unknown'\n  const needsReauth = /invalid_grant|token.*expired|revoked/i.test(reason)\n  throw new OAuthRefreshError(providerId, reason, { requiresReauth: needsReauth })\n}","handlingStrategy":"try-catch","validationCode":"// Cannot validate server-side refresh outcome client-side; best pre-check is token freshness.\nfunction isLikelyExpired(expiresAt: string | number, skewMs = 60_000): boolean {\n  const exp = typeof expiresAt === 'string' ? Date.parse(expiresAt) : expiresAt\n  return Date.now() + skewMs >= exp\n}","typeGuard":"function isOAuthRefreshFailure(result: unknown): result is { success: false; error?: string } {\n  return typeof result === 'object' && result !== null && (result as any).success === false\n}","tryCatchPattern":"try {\n  await oauthAdapter.refreshCredential(providerId, credential)\n} catch (error) {\n  const msg = error instanceof Error ? error.message : String(error)\n  if (/invalid_grant|revoked|expired/i.test(msg)) {\n    await startOAuthFlow(providerId) // re-authenticate\n  } else {\n    await backoffRetry(() => oauthAdapter.refreshCredential(providerId, credential))\n  }\n}","preventionTips":["Refresh tokens proactively before expiry rather than on-demand to avoid hitting the refresh path under load.","Inspect the main-process error string to distinguish permanent (re-auth) from transient (retry) failures.","Persist only the refresh_token; never assume a refresh will succeed indefinitely."],"tags":["oauth","authentication","token-refresh","ipc","credentials"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}