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

  1. Log or print the raw body of the refresh response to identify what is actually returned (HTML page vs empty vs binary).
  2. Check for proxy/TLS interception: try the same refresh request with curl -v and compare bodies.
  3. Disable or bypass any corporate proxy/WAF for api.xiaoyuzhoufm.com (NO_PROXY, VPN off-network).
  4. 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

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.

Related errors


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