{"record":{"id":"9ac087ec0e7c4879","repo":"chatboxai/chatbox","slug":"token-refresh-failed-text-9ac087","errorCode":null,"errorMessage":"Token refresh failed: ${text}","messagePattern":"Token refresh failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/oauth/providers/qwen.ts","lineNumber":186,"sourceCode":"    }\n\n    const response = await fetch(`${QWEN_BASE_URL}/api/v1/oauth2/token`, {\n      method: 'POST',\n      headers: {\n        Accept: 'application/json',\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: new URLSearchParams({\n        grant_type: 'refresh_token',\n        client_id: QWEN_CLIENT_ID,\n        refresh_token: credentials.refreshToken,\n      }).toString(),\n    })\n\n    if (!response.ok) {\n      const text = await response.text()\n      log.error('[OAuth:Qwen] Token refresh failed:', text)\n      throw new Error(`Token refresh failed: ${text}`)\n    }\n\n    const payload = (await response.json()) as {\n      access_token: string\n      refresh_token?: string\n      expires_in?: number\n    }\n\n    return {\n      accessToken: payload.access_token,\n      refreshToken: payload.refresh_token || credentials.refreshToken,\n      expiresAt: toExpiresAt(payload.expires_in),\n    }\n  },\n}\n","sourceCodeStart":168,"sourceCodeEnd":202,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/oauth/providers/qwen.ts#L168-L202","documentation":"Thrown by refreshToken() when the token endpoint responds with a non-OK status. Unlike authenticate(), this path does not parse a JSON error body — it reads the raw response text and embeds it verbatim. Refresh is called when stored credentials are near expiry and credentials.refreshToken is present (otherwise it returns the existing credentials with a warning).","triggerScenarios":"POST to ${QWEN_BASE_URL}/api/v1/oauth2/token with grant_type=refresh_token where the refresh_token has expired or been revoked (400 invalid_grant), the client_id mismatched (401), the network/proxy returned a gateway error (502/503), or Qwen rotated the refresh token on a prior call but the caller persisted the old one.","commonSituations":"Long-offline client whose refresh_token exceeded Qwen's refresh lifetime; the previous refreshToken response returned a new refresh_token but the store kept the old one (see line 197 which prefers payload.refresh_token || credentials.refreshToken — a store bug nullifies rotation); clock skew; QWEN_CLIENT_ID changed between releases.","solutions":["On 'invalid_grant' in the response text, clear stored Qwen credentials and re-run the full authenticate() device flow — refresh is unrecoverable.","Verify refresh-token rotation is persisted: after a successful refresh, save payload.refresh_token (the new one) rather than credentials.refreshToken.","If the response text is HTML/non-JSON, retry with exponential backoff — it is a transient server/proxy error, not a credential problem.","Confirm QWEN_CLIENT_ID matches the value used during the original authenticate(); a mismatch invalidates the refresh_token."],"exampleFix":"// before\nif (!response.ok) {\n  const text = await response.text()\n  throw new Error(`Token refresh failed: ${text}`)\n}\n\n// after: distinguish recoverable vs unrecoverable\nif (!response.ok) {\n  const text = await response.text()\n  if (text.includes('invalid_grant')) throw new QwenRefreshTokenRevokedError(text)\n  throw new Error(`Token refresh failed: ${text}`)\n}","handlingStrategy":"try-catch","validationCode":"if (!credentials?.refreshToken) { return reAuthenticate() }","typeGuard":"function isRefreshFailure(e: unknown): e is Error {\n  return e instanceof Error && e.message.startsWith('Token refresh failed:')\n}","tryCatchPattern":"try { return await qwenProvider.refreshToken(credentials) }\ncatch (e) {\n  if (isRefreshFailure(e) && /invalid_grant|expired/i.test(e.message)) { await clearQwenCredentials(); return qwenProvider.authenticate() }\n  throw e\n}","preventionTips":["Persist the rotated refresh_token returned by each successful refresh (payload.refresh_token).","Re-authenticate proactively when the refresh token is older than its expected lifetime.","Do not reuse a refresh token after a 401 on the resource API."],"tags":["oauth","qwen","refresh-token","network","typescript"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}