{"record":{"id":"9dd23212dec5dd3a","repo":"chatboxai/chatbox","slug":"no-refresh-token-available-9dd232","errorCode":null,"errorMessage":"No refresh token available","messagePattern":"No refresh token available","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/oauth/providers/openai.ts","lineNumber":89,"sourceCode":"    const { promise, close } = createCallbackServer(CALLBACK_PORT, signal, CALLBACK_HOST)\n\n    try {\n      await openUrl(authUrl.toString())\n      const result = await promise\n\n      if (result.state !== state) {\n        throw new Error('OAuth state mismatch')\n      }\n\n      return await exchangeCodeForTokens(result.code, verifier)\n    } finally {\n      close()\n    }\n  },\n\n  async refreshToken(credentials) {\n    if (!credentials.refreshToken) {\n      throw new Error('No refresh token available')\n    }\n\n    const response = await fetch(TOKEN_URL, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      body: new URLSearchParams({\n        grant_type: 'refresh_token',\n        refresh_token: credentials.refreshToken,\n        client_id: CLIENT_ID,\n      }),\n    })\n\n    if (!response.ok) {\n      const text = await response.text()\n      log.error('[OAuth:OpenAI] Token refresh failed:', text)\n      throw new Error(`Token refresh failed: ${response.status}`)\n    }\n","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/oauth/providers/openai.ts#L71-L107","documentation":"Thrown by OpenAI's refreshToken() when the stored credentials contain no refreshToken. OpenAI's flow relies on the offline_access scope to mint a refresh token; if that scope was not requested or the token exchange returned no refresh_token, subsequent refreshes will fail here. Unlike Qwen/MiniMax (which return the existing credentials), the OpenAI provider treats a missing refresh token as a hard error.","triggerScenarios":"The SCOPE constant is missing 'offline_access'; the token exchange response had no refresh_token field (e.g. consent was not granted for offline access); credentials were persisted from a flow that predated refresh-token support; storage migration dropped the field.","commonSituations":"Scope string edited to drop offline_access; older build's credentials loaded; user revoked offline consent at OpenAI; manual credential object missing the field in tests.","solutions":["Confirm SCOPE includes 'offline_access' before initiating login.","Before calling refreshToken, check credentials.refreshToken; if absent, trigger an interactive login() instead.","Verify exchangeCodeForTokens stores data.refresh_token and that persistence round-trips it.","On this error, clear stored credentials and re-run login() to obtain a fresh refresh-capable token set."],"exampleFix":"// before\nconst refreshed = await provider.refreshToken(credentials)\n\n// after\nif (!credentials.refreshToken) {\n  // OpenAI refresh requires offline_access; re-login to obtain it.\n  return startInteractiveLogin()\n}\nconst refreshed = await provider.refreshToken(credentials)","handlingStrategy":"validation","validationCode":"function hasRefreshToken(c: { refreshToken?: string | null }): boolean {\n  return typeof c.refreshToken === 'string' && c.refreshToken.length > 0\n}\nif (!hasRefreshToken(credentials)) {\n  return startInteractiveLogin()\n}\nawait provider.refreshToken(credentials)","typeGuard":"function hasRefreshToken(c: unknown): c is { refreshToken: string } {\n  return typeof c === 'object' && c !== null && typeof (c as any).refreshToken === 'string' && (c as any).refreshToken.length > 0\n}","tryCatchPattern":null,"preventionTips":["Confirm SCOPE includes 'offline_access' before initiating login.","Persist data.refresh_token from exchangeCodeForTokens wholesale into storage.","On a missing refresh token, trigger interactive login() rather than throwing at refresh time."],"tags":["oauth","validation","credentials","openai","refresh-token"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}