chatboxai/chatbox · error · Error

OAuth credential missing for provider

Error message

OAuth credential missing for provider: ${chatboxProviderId}

What it means

Thrown inside the credential-manager's refreshCredential closure when the in-memory `credential` is undefined at the moment a refresh is attempted. Because refreshCredential is only called from getCredential (which itself throws when credential is missing — see 238), reaching this branch implies the credential was cleared concurrently between the getCredential check and the refresh call (a race with clear()).

Solutions

  1. Treat this error as 'session ended mid-refresh': discard the in-flight operation and prompt re-authentication rather than retrying.
  2. Serialize credential mutations (e.g. a mutex around set/clear/refresh) so refresh cannot observe a half-cleared state.
  3. Have getCredential hold a local reference to the credential it validated and pass it into refreshCredential, eliminating the re-read race.
  4. In the UI, surface a 'You've been signed out' notice when this error is caught and disable provider actions until re-login.

Example fix

// before
const refreshCredential = async (): Promise<OAuthCredentials> => {
  if (!credential) {
    throw new Error(`OAuth credential missing for provider: ${chatboxProviderId}`)
  }
  ...
}
// after — capture the reference so a concurrent clear doesn't break refresh
const refreshCredential = async (): Promise<OAuthCredentials> => {
  const current = credential
  if (!current) {
    throw new Error(`OAuth credential missing for provider: ${chatboxProviderId}`)
  }
  ...dependencies.oauth.refreshCredential(oauthProviderId, current)...
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Serialize credential mutations to eliminate the refresh/clear race.
let credentialLock: Promise<unknown> = Promise.resolve()
function withCredentialLock<T>(fn: () => Promise<T>): Promise<T> {
  const next = credentialLock.then(fn, fn)
  credentialLock = next.catch(() => {})
  return next
}

Type guard

function isOAuthCredentialMissing(e: unknown, providerId: string): boolean {
  return e instanceof Error && e.message === `OAuth credential missing for provider: ${providerId}`
}

Try / catch

try {
  return await credentialManager.getAccessToken()
} catch (e) {
  if (e instanceof Error && e.message.startsWith('OAuth credential missing for provider:')) {
    // session ended mid-refresh; prompt re-login, do not retry
    redirectToLogin()
    return
  }
  throw e
}

Prevention

When it happens

Trigger: getCredential saw a credential and called refreshCredential; before refreshCredential's first statement ran (or in a concurrent task), clear() set credential to undefined. The synchronous guard at the top of refreshCredential then throws.

Common situations: User signed out (clear) while a token refresh was already queued; a 401 elsewhere triggered clear() racing an in-flight refresh; multi-tab logout clearing shared credential state; an explicit re-login replaced credential state and a stale refresh promise ran.

Related errors


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

Appendix: source

Thrown at src/shared/oauth/credential-manager.ts:49

    return undefined
  }

  let credential: OAuthCredentials | undefined = providerSetting.oauth
  let refreshPromise: Promise<OAuthCredentials> | undefined

  const persistCredential = (nextCredential: OAuthCredentials) => {
    credential = nextCredential
    dependencies.oauth?.persistCredential(settingsProviderId, nextCredential)
  }

  const clearCredential = () => {
    credential = undefined
    dependencies.oauth?.clearCredential(settingsProviderId)
  }

  const refreshCredential = async (): Promise<OAuthCredentials> => {
    if (!credential) {
      throw new Error(`OAuth credential missing for provider: ${chatboxProviderId}`)
    }
    if (!dependencies.oauth) {
      return credential
    }
    if (!refreshPromise) {
      refreshPromise = dependencies.oauth
        .refreshCredential(oauthProviderId, credential)
        .then((nextCredential) => {
          persistCredential(nextCredential)
          return nextCredential
        })
        .finally(() => {
          refreshPromise = undefined
        })
    }
    return refreshPromise
  }

View on GitHub (pinned to 81571269ad)