{"record":{"id":"0cea1a55a7f9dbb5","repo":"chatboxai/chatbox","slug":"oauth-credential-missing-for-provider-chatboxpr","errorCode":null,"errorMessage":"OAuth credential missing for provider: ${chatboxProviderId}","messagePattern":"OAuth credential missing for provider: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/shared/oauth/credential-manager.ts","lineNumber":49,"sourceCode":"    return undefined\n  }\n\n  let credential: OAuthCredentials | undefined = providerSetting.oauth\n  let refreshPromise: Promise<OAuthCredentials> | undefined\n\n  const persistCredential = (nextCredential: OAuthCredentials) => {\n    credential = nextCredential\n    dependencies.oauth?.persistCredential(settingsProviderId, nextCredential)\n  }\n\n  const clearCredential = () => {\n    credential = undefined\n    dependencies.oauth?.clearCredential(settingsProviderId)\n  }\n\n  const refreshCredential = async (): Promise<OAuthCredentials> => {\n    if (!credential) {\n      throw new Error(`OAuth credential missing for provider: ${chatboxProviderId}`)\n    }\n    if (!dependencies.oauth) {\n      return credential\n    }\n    if (!refreshPromise) {\n      refreshPromise = dependencies.oauth\n        .refreshCredential(oauthProviderId, credential)\n        .then((nextCredential) => {\n          persistCredential(nextCredential)\n          return nextCredential\n        })\n        .finally(() => {\n          refreshPromise = undefined\n        })\n    }\n    return refreshPromise\n  }\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/shared/oauth/credential-manager.ts#L31-L67","documentation":"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()).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Treat this error as 'session ended mid-refresh': discard the in-flight operation and prompt re-authentication rather than retrying.","Serialize credential mutations (e.g. a mutex around set/clear/refresh) so refresh cannot observe a half-cleared state.","Have getCredential hold a local reference to the credential it validated and pass it into refreshCredential, eliminating the re-read race.","In the UI, surface a 'You've been signed out' notice when this error is caught and disable provider actions until re-login."],"exampleFix":"// before\nconst refreshCredential = async (): Promise<OAuthCredentials> => {\n  if (!credential) {\n    throw new Error(`OAuth credential missing for provider: ${chatboxProviderId}`)\n  }\n  ...\n}\n// after — capture the reference so a concurrent clear doesn't break refresh\nconst refreshCredential = async (): Promise<OAuthCredentials> => {\n  const current = credential\n  if (!current) {\n    throw new Error(`OAuth credential missing for provider: ${chatboxProviderId}`)\n  }\n  ...dependencies.oauth.refreshCredential(oauthProviderId, current)...\n}","handlingStrategy":"try-catch","validationCode":"// Serialize credential mutations to eliminate the refresh/clear race.\nlet credentialLock: Promise<unknown> = Promise.resolve()\nfunction withCredentialLock<T>(fn: () => Promise<T>): Promise<T> {\n  const next = credentialLock.then(fn, fn)\n  credentialLock = next.catch(() => {})\n  return next\n}","typeGuard":"function isOAuthCredentialMissing(e: unknown, providerId: string): boolean {\n  return e instanceof Error && e.message === `OAuth credential missing for provider: ${providerId}`\n}","tryCatchPattern":"try {\n  return await credentialManager.getAccessToken()\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('OAuth credential missing for provider:')) {\n    // session ended mid-refresh; prompt re-login, do not retry\n    redirectToLogin()\n    return\n  }\n  throw e\n}","preventionTips":["Serialize set/clear/refresh with a mutex so refresh can't observe a half-cleared state.","Pass the credential reference into refreshCredential rather than re-reading the field.","Treat this specific error as terminal for the operation — never auto-retry; prompt re-authentication."],"tags":["oauth","race-condition","credential-lifecycle","auth"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}