chatboxai/chatbox · error · Error

Token refresh failed: ${text}

Error message

Token refresh failed: ${text}

What it means

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

Source

Thrown at src/main/oauth/providers/qwen.ts:186

    }

    const response = await fetch(`${QWEN_BASE_URL}/api/v1/oauth2/token`, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: QWEN_CLIENT_ID,
        refresh_token: credentials.refreshToken,
      }).toString(),
    })

    if (!response.ok) {
      const text = await response.text()
      log.error('[OAuth:Qwen] Token refresh failed:', text)
      throw new Error(`Token refresh failed: ${text}`)
    }

    const payload = (await response.json()) as {
      access_token: string
      refresh_token?: string
      expires_in?: number
    }

    return {
      accessToken: payload.access_token,
      refreshToken: payload.refresh_token || credentials.refreshToken,
      expiresAt: toExpiresAt(payload.expires_in),
    }
  },
}

View on GitHub (pinned to 81571269ad)

Solutions

  1. On 'invalid_grant' in the response text, clear stored Qwen credentials and re-run the full authenticate() device flow — refresh is unrecoverable.
  2. Verify refresh-token rotation is persisted: after a successful refresh, save payload.refresh_token (the new one) rather than credentials.refreshToken.
  3. If the response text is HTML/non-JSON, retry with exponential backoff — it is a transient server/proxy error, not a credential problem.
  4. Confirm QWEN_CLIENT_ID matches the value used during the original authenticate(); a mismatch invalidates the refresh_token.

Example fix

// before
if (!response.ok) {
  const text = await response.text()
  throw new Error(`Token refresh failed: ${text}`)
}

// after: distinguish recoverable vs unrecoverable
if (!response.ok) {
  const text = await response.text()
  if (text.includes('invalid_grant')) throw new QwenRefreshTokenRevokedError(text)
  throw new Error(`Token refresh failed: ${text}`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!credentials?.refreshToken) { return reAuthenticate() }

Type guard

function isRefreshFailure(e: unknown): e is Error {
  return e instanceof Error && e.message.startsWith('Token refresh failed:')
}

Try / catch

try { return await qwenProvider.refreshToken(credentials) }
catch (e) {
  if (isRefreshFailure(e) && /invalid_grant|expired/i.test(e.message)) { await clearQwenCredentials(); return qwenProvider.authenticate() }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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