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
- 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.
- For 404/400, verify the endpoint path and parameters (e.g. correct eid/pid) against the current API.
- For 429, add rate limiting (e.g. delay between requests) or honor Retry-After.
- 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
- Distinguish retryable statuses (429, 5xx) from client errors (400, 404) and only retry the former.
- Honor rate limits: throttle polling loops and respect Retry-After headers to avoid 429s.
- Validate endpoint paths and query parameters (eid/pid) before calling; keep endpoints current with API changes.
- Treat 401/403 separately — those arrive as AUTH_REQUIRED CliError, not this CommandExecutionError.
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
- HTTP_ERROR
- Lobsters API HTTP ${res.status} for story ${shortId}
- ${label} returned HTTP ${resp.status}: ${summarizeApiError(p
- autohome ${contextHint} HTTP ${resp.status}
- HTTP ${result.httpStatus} from /api/organizations
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/a8041a7fe5fbe8f8.
Report an issue: GitHub.