jackwener/OpenCLI · error · CommandExecutionError

Failed to reach Xiaoyuzhou API: ${getErrorMessage(error)}

Error message

Failed to reach Xiaoyuzhou API: ${getErrorMessage(error)}

What it means

Thrown by performXiaoyuzhouJsonRequest when the fetch call to a Xiaoyuzhou API endpoint throws before an HTTP response exists — network unreachable, DNS failure, TLS error, or the 20-second AbortSignal.timeout firing. It is wrapped as CommandExecutionError; auth problems are intentionally NOT this error (those surface later as AUTH_REQUIRED CliErrors).

Source

Thrown at clis/xiaoyuzhou/auth.js:204

    const {
        method = 'GET',
        query,
        body,
    } = options;
    let response;
    try {
        response = await fetchImpl(buildApiUrl(endpoint, query), {
            method,
            headers: buildXiaoyuzhouHeaders(credentials, {
                contentType: 'application/json',
                includeLocalTime: true,
            }),
            body: body === undefined ? undefined : JSON.stringify(body),
            signal: AbortSignal.timeout(20_000),
        });
    }
    catch (error) {
        throw new CommandExecutionError(`Failed to reach Xiaoyuzhou API: ${getErrorMessage(error)}`);
    }
    return response;
}

export async function requestXiaoyuzhouJson(endpoint, options = {}, fetchImpl = fetch) {
    let credentials = options.credentials ?? loadXiaoyuzhouCredentials();
    if (shouldRefreshXiaoyuzhouCredentials(credentials)) {
        credentials = await refreshXiaoyuzhouCredentials(credentials, fetchImpl);
    }
    let response = await performXiaoyuzhouJsonRequest(endpoint, options, credentials, fetchImpl);
    if (response.status === 401) {
        credentials = await refreshXiaoyuzhouCredentials(credentials, fetchImpl);
        response = await performXiaoyuzhouJsonRequest(endpoint, options, credentials, fetchImpl);
    }
    const bodyText = await response.text();
    if (!response.ok) {
        if (response.status === 401 || response.status === 403) {
            throw createXiaoyuzhouAuthError(`Xiaoyuzhou API rejected the credentials with HTTP ${response.status}`);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify reachability: curl -v https://api.xiaoyuzhoufm.com — fix connectivity, DNS, or firewall before retrying.
  2. If timeouts recur, pass a custom fetchImpl with a longer AbortSignal timeout.
  3. Add retry with exponential backoff for transient network errors around requestXiaoyuzhouJson calls.
  4. Confirm Node >= 18 so global fetch is available.

Example fix

// before: single attempt, hard failure on flaky network
const { data } = await requestXiaoyuzhouJson('/episodes/get', { query: { eid } });
// after: bounded retry on transport errors
async function fetchWithRetry(endpoint, options) {
  for (let attempt = 0; ; attempt++) {
    try { return await requestXiaoyuzhouJson(endpoint, options); }
    catch (e) {
      if (!String(e.message).includes('Failed to reach') || attempt >= 2) throw e;
      await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
    }
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// network preflight
async function assertApiReachable() {
  try {
    await fetch('https://api.xiaoyuzhoufm.com', { method: 'HEAD', signal: AbortSignal.timeout(5000) });
  } catch (e) {
    throw new Error(`Xiaoyuzhou API unreachable (${e.message}). Check network, DNS, proxy, and Node >= 18.`);
  }
}

Type guard

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

Try / catch

import { CommandExecutionError } from '@jackwener/opencli/errors';
async function withRetry(endpoint, options, attempts = 3) {
  for (let i = 0; ; i++) {
    try {
      return await requestXiaoyuzhouJson(endpoint, options);
    } catch (err) {
      const transient = err instanceof CommandExecutionError
        && err.message.startsWith('Failed to reach Xiaoyuzhou API:');
      if (!transient || i >= attempts - 1) throw err;
      await new Promise(r => setTimeout(r, 500 * 2 ** i));
    }
  }
}

Prevention

When it happens

Trigger: Any requestXiaoyuzhouJson call (episodes, transcripts, history, progress) where the TCP/TLS connection to api.xiaoyuzhoufm.com cannot be established or the request exceeds 20s: offline machine, DNS outage, firewall, or hung connection.

Common situations: No internet / Wi-Fi dropped; DNS misconfiguration; IPv6-only or broken dual-stack network; firewalled corporate network; slow VPN causing the 20s timeout; Node < 18 lacking global fetch (fetchImpl undefined → TypeError wrapped here).

Related errors


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