jackwener/OpenCLI · error · CommandExecutionError

Xiaoyuzhou API request failed with HTTP ${response.status}${

Error message

Xiaoyuzhou API request failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}

What it means

Thrown by requestXiaoyuzhouJson when the API responds with a non-OK HTTP status other than 401/403 (those become AUTH_REQUIRED errors instead, after one automatic refresh+retry on 401). CommandExecutionError carries the status code and any response body, meaning credentials were accepted/irrelevant but the request itself failed — typically a bad endpoint, invalid parameters, 404, 429 rate limit, or 5xx server error.

Source

Thrown at clis/xiaoyuzhou/auth.js:224

    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}`);
        }
        throw new CommandExecutionError(`Xiaoyuzhou API request failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}`);
    }
    let parsed;
    try {
        parsed = JSON.parse(bodyText);
    }
    catch (error) {
        throw new CommandExecutionError(`Xiaoyuzhou API returned invalid JSON: ${getErrorMessage(error)}`);
    }
    const serviceCode = parsed?.code;
    if (serviceCode !== undefined && serviceCode !== null) {
        const numericCode = Number(serviceCode);
        if (!Number.isFinite(numericCode)) {
            throw new CommandExecutionError('Xiaoyuzhou API returned an invalid service code');
        }
        if (numericCode === 401 || numericCode === 403) {
            throw createXiaoyuzhouAuthError(`Xiaoyuzhou API rejected the credentials with service code ${numericCode}`);
        }
        if (numericCode !== 0 && numericCode !== 200) {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the status code and body in the message: 429 → back off and retry with exponential delay; 5xx → retry later; 400/404 → fix the request.
  2. For 404/400, verify the endpoint path and parameters (e.g. correct eid/pid) against the current API.
  3. For 429, add rate limiting (e.g. delay between requests) or honor Retry-After.
  4. For persistent 5xx, check Xiaoyuzhou service status / retry with backoff via a custom fetchImpl.

Example fix

// before: blind call, 429 kills the script
const { data } = await requestXiaoyuzhouJson('/episode/feed', { query: { eid } });
// after: handle retryable statuses explicitly
async function callApi(endpoint, options) {
  try { return await requestXiaoyuzhouJson(endpoint, options); }
  catch (e) {
    const m = /HTTP (\d{3})/.exec(e.message);
    const status = m ? Number(m[1]) : 0;
    if (status === 429 || status >= 500) {
      await new Promise(r => setTimeout(r, 5000));
      return requestXiaoyuzhouJson(endpoint, options);
    }
    throw e;
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// validate inputs before calling the API to avoid 400/404-class failures
function assertEpisodeId(eid) {
  if (typeof eid !== 'string' || !/^[0-9a-f-]{10,}$/i.test(eid.trim())) {
    throw new Error(`Invalid episode id '${eid}' — request would fail with HTTP 400/404.`);
  }
}

Type guard

function isApiHttpError(err) {
  if (!(err instanceof Error)) return false;
  const m = /^Xiaoyuzhou API request failed with HTTP (\d{3})/.exec(err.message);
  return m !== null ? { status: Number(m[1]) } : false;
}

Try / catch

import { CommandExecutionError, CliError } from '@jackwener/opencli/errors';
try {
  const { data } = await requestXiaoyuzhouJson('/episodes/get', { query: { eid } });
} catch (err) {
  if (err instanceof CommandExecutionError) {
    const status = Number(/HTTP (\d{3})/.exec(err.message)?.[1] ?? 0);
    if (status === 429 || status >= 500) {
      // retryable: back off and try again
      await new Promise(r => setTimeout(r, 5000));
      return requestXiaoyuzhouJson('/episodes/get', { query: { eid } });
    }
    if (status === 400 || status === 404) {
      console.error('Bad request: verify the endpoint path and parameters.', err.message);
    }
  }
  throw err;
}

Prevention

When it happens

Trigger: requestXiaoyuzhouJson against an endpoint that returns e.g. HTTP 400 (malformed query/body), 404 (wrong episode/endpoint path), 429 (too many requests), or 5xx (Xiaoyuzhou server outage) after the 401-refresh-retry path did not apply.

Common situations: Passing an invalid or deleted episode id (404); hard-coded endpoint paths broken by an API version change; scripting that polls the API too aggressively (429); transient Xiaoyuzhou outages (502/503); sending a body the server rejects (400).

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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