jackwener/OpenCLI · error · CommandExecutionError
Xiaoyuzhou refresh returned invalid JSON: ${getErrorMessage(
Error message
Xiaoyuzhou refresh returned invalid JSON: ${getErrorMessage(error)} What it means
Thrown by refreshXiaoyuzhouCredentials when the token-refresh endpoint returned HTTP 200 but the body is not parseable JSON. The library expects { success, "x-jike-access-token", "x-jike-refresh-token" } and raises CommandExecutionError instead of silently using garbage. This indicates an unexpected/intermediary response rather than expired credentials.
Source
Thrown at clis/xiaoyuzhou/auth.js:155
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'] || '',
refresh_token: parsed['x-jike-refresh-token'] || '',
expires_at: getNowMs() + XIAOYUZHOU_TOKEN_TTL_MS,
});
if (!nextCredentials.access_token || !nextCredentials.refresh_token) {
throw createXiaoyuzhouAuthError('Xiaoyuzhou refresh API returned empty access_token or refresh_token');
}
saveXiaoyuzhouCredentials(nextCredentials);
return nextCredentials;
}
function buildApiUrl(endpoint, query) {View on GitHub (pinned to 49907e53dc)
Solutions
- Log or print the raw body of the refresh response to identify what is actually returned (HTML page vs empty vs binary).
- Check for proxy/TLS interception: try the same refresh request with curl -v and compare bodies.
- Disable or bypass any corporate proxy/WAF for api.xiaoyuzhoufm.com (NO_PROXY, VPN off-network).
- If using a custom fetchImpl/mock in tests, make it return a JSON body with success:true.
Example fix
// before: raw error with no body context
// Failed to refresh -> Xiaoyuzhou refresh returned invalid JSON: Unexpected token '<'...
// after: capture the body to diagnose
try {
await refreshXiaoyuzhouCredentials(creds);
} catch (e) {
console.error('refresh failed:', e.message);
const probe = await fetch('https://api.xiaoyuzhoufm.com/app_auth_tokens.refresh', { method: 'HEAD' });
console.error('status:', probe.status, 'content-type:', probe.headers.get('content-type'));
} Defensive patterns
Strategy: try-catch
Validate before calling
// probe the endpoint and confirm the response is JSON before refreshing
const probe = await fetch('https://api.xiaoyuzhoufm.com/app_auth_tokens.refresh', { method: 'OPTIONS' }).catch(() => null);
const contentType = probe?.headers?.get('content-type') ?? '';
if (probe && !contentType.includes('application/json')) {
throw new Error(`Refresh endpoint answered with content-type '${contentType}' — a proxy/WAF or captive portal is likely intercepting traffic.`);
} Type guard
function isRefreshPayload(value) {
return typeof value === 'object' && value !== null
&& typeof value.success === 'boolean'
&& (value['x-jike-access-token'] === undefined || typeof value['x-jike-access-token'] === 'string');
} Try / catch
import { CommandExecutionError } from '@jackwener/opencli/errors';
try {
creds = await refreshXiaoyuzhouCredentials(creds);
} catch (err) {
if (err instanceof CommandExecutionError && err.message.includes('refresh returned invalid JSON')) {
console.error('Non-JSON body from refresh endpoint — inspect for proxy/WAF/captive-portal interception.');
// capture body for diagnosis before failing
}
throw err;
} Prevention
- Check content-type of API responses when debugging; HTML bodies almost always mean an intermediary (proxy/WAF/portal), not the API.
- Bypass or allowlist TLS-inspecting corporate proxies for api.xiaoyuzhoufm.com.
- Avoid aggressive request rates that trip WAF block pages.
- When stubbing fetch in tests, always return parseable JSON bodies.
When it happens
Trigger: POST to /app_auth_tokens.refresh succeeds at HTTP level but response.text() yields HTML, empty string, or compressed/garbled bytes — typically a captive portal, proxy error page, CDN/WAF block page, or wrong content-encoding inserted between client and api.xiaoyuzhoufm.com.
Common situations: Hotel/airport Wi-Fi captive portal intercepting HTTPS (less common) or a corporate TLS-inspecting proxy returning its own error page; WAF/rate-limiter returning an HTML block page with 200; a misconfigured local mock/fetchImpl returning non-JSON.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ${label} returned invalid JSON
- ${label} returned malformed JSON: ${err?.message ?? err}
- ${label} returned malformed JSON: ${err?.message ?? err}
- ${label} returned malformed JSON: ${err?.message ?? err}
- Malformed JSON from Stack Exchange API for ${label}: ${detai
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/5116884e9e87f663.
Report an issue: GitHub.