jackwener/OpenCLI · error · CommandExecutionError

Failed to refresh Xiaoyuzhou credentials: ${getErrorMessage(

Error message

Failed to refresh Xiaoyuzhou credentials: ${getErrorMessage(error)}

What it means

Thrown by refreshXiaoyuzhouCredentials when the POST to https://api.xiaoyuzhoufm.com/app_auth_tokens.refresh fails at the network/transport level (fetch itself throws). It wraps the underlying error (DNS failure, TLS problem, timeout of the 20s AbortSignal, connection reset) in a CommandExecutionError. The refresh request never got an HTTP response, so no tokens were rotated or saved.

Source

Thrown at clis/xiaoyuzhou/auth.js:144

export async function refreshXiaoyuzhouCredentials(credentials, fetchImpl = fetch) {
    if (!credentials.refresh_token) {
        throw createXiaoyuzhouAuthError('Xiaoyuzhou refresh token is missing');
    }
    let response;
    try {
        response = await fetchImpl(`${XIAOYUZHOU_API_BASE_URL}/app_auth_tokens.refresh`, {
            method: 'POST',
            headers: buildXiaoyuzhouHeaders(credentials, {
                contentType: 'application/x-www-form-urlencoded; charset=utf-8',
                includeLocalTime: true,
                includeRefreshToken: true,
            }),
            signal: AbortSignal.timeout(20_000),
        });
    }
    catch (error) {
        throw new CommandExecutionError(`Failed to refresh Xiaoyuzhou credentials: ${getErrorMessage(error)}`);
    }
    const bodyText = await response.text();
    if (!response.ok) {
        throw createXiaoyuzhouAuthError(`Xiaoyuzhou token refresh failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}`);
    }
    let parsed;
    try {
        parsed = JSON.parse(bodyText);
    }
    catch (error) {
        throw new CommandExecutionError(`Xiaoyuzhou refresh returned invalid JSON: ${getErrorMessage(error)}`);
    }
    if (!parsed?.success) {
        throw createXiaoyuzhouAuthError('Xiaoyuzhou refresh API returned success=false');
    }
    const nextCredentials = normalizeXiaoyuzhouCredentials({
        ...credentials,
        access_token: parsed['x-jike-access-token'] || '',

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check basic connectivity: curl -I https://api.xiaoyuzhoufm.com — fix network/DNS/proxy first.
  2. If behind a proxy, set HTTPS_PROXY/HTTP_PROXY or configure the fetch impl accordingly.
  3. Retry later if it was a transient timeout; the 20s hard limit can be worked around by passing a custom fetchImpl with a longer AbortSignal.
  4. Verify Node >= 18 so global fetch exists (or pass a fetchImpl explicitly).

Example fix

// before: implicit global fetch with 20s timeout
await refreshXiaoyuzhouCredentials(creds);
// after: retry with custom fetch and longer timeout on transient network errors
const fetchWithRetry = async (url, init) => {
  for (let i = 0; i < 3; i++) {
    try { return await fetch(url, { ...init, signal: AbortSignal.timeout(60_000) }); }
    catch (e) { if (i === 2) throw e; await new Promise(r => setTimeout(r, 1000 * (i + 1))); }
  }
};
await refreshXiaoyuzhouCredentials(creds, fetchWithRetry);
Defensive patterns

Strategy: retry

Validate before calling

// preflight connectivity check before making API calls
const reachable = await fetch('https://api.xiaoyuzhoufm.com', { method: 'HEAD', signal: AbortSignal.timeout(5000) })
  .then(() => true)
  .catch(() => false);
if (!reachable) throw new Error('api.xiaoyuzhoufm.com is unreachable — check network/proxy/VPN before retrying.');

Type guard

function isTransportError(err) {
  return err instanceof Error && err.message.startsWith('Failed to refresh Xiaoyuzhou credentials:');
}

Try / catch

import { CommandExecutionError } from '@jackwener/opencli/errors';
try {
  await refreshXiaoyuzhouCredentials(creds);
} catch (err) {
  if (err instanceof CommandExecutionError && err.message.startsWith('Failed to refresh Xiaoyuzhou credentials')) {
    // transient network/timeout — retry with backoff
    await new Promise(r => setTimeout(r, 2000));
    return refreshXiaoyuzhouCredentials(creds);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling requestXiaoyuzhouJson when shouldRefreshXiaoyuzhouCredentials is true (token within 60s skew of expiry) or after a 401, and the fetch to app_auth_tokens.refresh throws: no internet, DNS failure, firewall/proxy blocking api.xiaoyuzhoufm.com, or the 20-second AbortSignal.timeout firing.

Common situations: Offline laptop or flaky Wi-Fi; corporate proxy stripping/blocking the request; VPN needed to reach the API from certain regions; slow network causing the 20s timeout to elapse; Node without global fetch (Node < 18) making fetchImpl undefined.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/4da29d8c8afcb5ac. Report an issue: GitHub.