chatboxai/chatbox · error · Error

Failed to refresh OAuth credential for ${providerId}

Error message

Failed to refresh OAuth credential for ${providerId}

What it means

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).

Source

Thrown at src/renderer/adapters/index.ts:149

  const maybeDesktopPlatform = platform as unknown as { ipc?: OAuthIpcInvoker }
  if (!maybeDesktopPlatform.ipc) {
    throw new Error('OAuth IPC is only available on desktop')
  }
  return maybeDesktopPlatform.ipc
}

function createDesktopOAuthAdapter(oauthIpc?: OAuthIpcInvoker): OAuthAdapter {
  return {
    async refreshCredential(providerId: string, credential: OAuthCredentials): Promise<OAuthCredentials> {
      const ipc = oauthIpc ?? getDefaultOAuthIpc()
      const resultJson = await ipc.invoke(OAuthIpcChannels.REFRESH, providerId, JSON.stringify(credential))
      const result = JSON.parse(resultJson) as {
        success: boolean
        credentials?: OAuthCredentials
        error?: string
      }
      if (!result.success || !result.credentials) {
        throw new Error(result.error || `Failed to refresh OAuth credential for ${providerId}`)
      }
      return result.credentials
    },
    persistCredential(providerId: string, credential: OAuthCredentials): void {
      const settingsProviderId = toOAuthSettingsProviderId(providerId) || providerId
      settingsStore.setState((currentSettings) => ({
        providers: {
          ...(currentSettings.providers || {}),
          [settingsProviderId]: {
            ...(currentSettings.providers?.[settingsProviderId] || {}),
            oauth: credential,
          },
        },
      }))
    },
    clearCredential(providerId: string): void {
      const settingsProviderId = toOAuthSettingsProviderId(providerId) || providerId
      settingsStore.setState((currentSettings) => {

View on GitHub (pinned to 81571269ad)

Solutions

  1. Prompt the user to re-authenticate (start a fresh OAuth flow) since the refresh token is no longer usable.
  2. Inspect result.error from the main process for invalid_grant or token_expired to decide between re-auth and retry.
  3. If the failure is transient (network), retry once with backoff before forcing re-auth.

Example fix

// before
if (!result.success || !result.credentials) {
  throw new Error(result.error || `Failed to refresh OAuth credential for ${providerId}`)
}

// after — surface a typed error so the UI can distinguish re-auth from retry
if (!result.success || !result.credentials) {
  const reason = result.error || 'unknown'
  const needsReauth = /invalid_grant|token.*expired|revoked/i.test(reason)
  throw new OAuthRefreshError(providerId, reason, { requiresReauth: needsReauth })
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Cannot validate server-side refresh outcome client-side; best pre-check is token freshness.
function isLikelyExpired(expiresAt: string | number, skewMs = 60_000): boolean {
  const exp = typeof expiresAt === 'string' ? Date.parse(expiresAt) : expiresAt
  return Date.now() + skewMs >= exp
}

Type guard

function isOAuthRefreshFailure(result: unknown): result is { success: false; error?: string } {
  return typeof result === 'object' && result !== null && (result as any).success === false
}

Try / catch

try {
  await oauthAdapter.refreshCredential(providerId, credential)
} catch (error) {
  const msg = error instanceof Error ? error.message : String(error)
  if (/invalid_grant|revoked|expired/i.test(msg)) {
    await startOAuthFlow(providerId) // re-authenticate
  } else {
    await backoffRetry(() => oauthAdapter.refreshCredential(providerId, credential))
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/467f6b7f229e3076. Report an issue: GitHub.