chatboxai/chatbox · error · Error

OAuth credential missing for provider: ${chatboxProviderId}

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

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)

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.