decolua/9router · error

xAI token refresh failed: ${err}

Error message

xAI token refresh failed: ${err}

What it means

refreshAccessToken posts a refresh_token grant to xAI's token endpoint to obtain a new access token. On a non-ok response it throws 'xAI token refresh failed: <body>' with the upstream body text. A failure here usually means the refresh token is invalid, expired, or has been rotated/revoked.

Source

Thrown at src/lib/oauth/services/xai.js:171

   * Refresh an access token using a refresh_token.
   */
  async refreshAccessToken(refreshToken) {
    const { tokenUrl } = await discoverEndpoints();
    const res = await fetch(tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
      },
      body: new URLSearchParams({
        grant_type: "refresh_token",
        client_id: XAI_CONFIG.clientId,
        refresh_token: refreshToken,
      }),
    });
    if (!res.ok) {
      const err = await res.text();
      throw new Error(`xAI token refresh failed: ${err}`);
    }
    return await res.json();
  }

  /**
   * Complete xAI OAuth flow end-to-end (CLI entrypoint).
   * Returns the raw token response plus extracted email.
   */
  async connect() {
    const spinner = createSpinner("Starting xAI OAuth...").start();
    try {
      spinner.text = "Discovering xAI endpoints...";
      const { authorizeUrl, tokenUrl } = await discoverEndpoints();

      spinner.text = `Starting local server on port ${XAI_CONFIG.loopbackPort}...`;
      let callbackParams = null;
      const { port, close } = await startLocalServer((params) => {
        callbackParams = params;

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Inspect the thrown body: invalid_grant means re-authentication is required — the refresh token cannot be recovered.
  2. Persist the NEW refresh token immediately after each successful refresh if xAI rotates them.
  3. Guard against concurrent refreshes of the same account (single-flight/lock) to avoid burning rotated tokens.
  4. If 5xx, retry with backoff — the token may still be valid.

Example fix

// before
const t = await refreshAccessToken(stored.refreshToken); // throws when expired
// after
try {
  const t = await refreshAccessToken(stored.refreshToken);
  saveNewRefreshToken(t.refresh_token ?? stored.refreshToken);
} catch (e) {
  if (e.message.includes('invalid_grant')) return requireReauthorization(account);
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (typeof refreshToken !== 'string' || !refreshToken) {
  return requireReauthorization(account); // nothing to refresh with
}

Try / catch

try {
  const tokens = await refreshAccessToken(refreshToken);
  if (tokens.refresh_token && tokens.refresh_token !== refreshToken) {
    await persistRotatedToken(account, tokens.refresh_token); // rotation: save immediately
  }
} catch (e) {
  if (!e.message.startsWith('xAI token refresh failed:')) throw e;
  if (e.message.includes('invalid_grant')) return requireReauthorization(account); // expired/revoked
  if (/HTTP 5\d\d|fetch failed/.test(e.message)) return retryWithBackoff();
  throw e;
}

Prevention

When it happens

Trigger: Refresh POST returns 400 (invalid_grant — refresh token expired or revoked, or already used after rotation), 401 (invalid_client), or 5xx from xAI.

Common situations: Long-lived stored credentials finally expiring; xAI rotating refresh tokens on each use and the app reusing an old one after a concurrent refresh; user revoking the app in their xAI account settings.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/164c9ccdd3ca1290. Report an issue: GitHub.