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
- Check basic connectivity: curl -I https://api.xiaoyuzhoufm.com — fix network/DNS/proxy first.
- If behind a proxy, set HTTPS_PROXY/HTTP_PROXY or configure the fetch impl accordingly.
- 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.
- 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
- Refresh tokens proactively well before expiry so refreshes happen at a controlled time, not mid-request.
- Wrap refresh calls in exponential-backoff retry for AbortError/timeout and ECONNRESET-style failures.
- Ensure the environment has outbound HTTPS to api.xiaoyuzhoufm.com (firewall/proxy allowlist, VPN when needed).
- Use Node >= 18 (or inject fetchImpl) so global fetch and AbortSignal.timeout exist.
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
- FETCH_ERROR
- archive wayback request failed: ${error?.message || error}
- Failed to fetch Chess.com API ${url}: ${error?.message || er
- Failed to reach Xiaoyuzhou API: ${getErrorMessage(error)}
- Failed to fetch Xiaoyuzhou transcript content: ${getErrorMes
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/4da29d8c8afcb5ac.
Report an issue: GitHub.