{"record":{"id":"5116884e9e87f663","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-refresh-returned-invalid-json-geterr","errorCode":null,"errorMessage":"Xiaoyuzhou refresh returned invalid JSON: ${getErrorMessage(error)}","messagePattern":"Xiaoyuzhou refresh returned invalid JSON: (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/auth.js","lineNumber":155,"sourceCode":"                includeLocalTime: true,\n                includeRefreshToken: true,\n            }),\n            signal: AbortSignal.timeout(20_000),\n        });\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Failed to refresh Xiaoyuzhou credentials: ${getErrorMessage(error)}`);\n    }\n    const bodyText = await response.text();\n    if (!response.ok) {\n        throw createXiaoyuzhouAuthError(`Xiaoyuzhou token refresh failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}`);\n    }\n    let parsed;\n    try {\n        parsed = JSON.parse(bodyText);\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Xiaoyuzhou refresh returned invalid JSON: ${getErrorMessage(error)}`);\n    }\n    if (!parsed?.success) {\n        throw createXiaoyuzhouAuthError('Xiaoyuzhou refresh API returned success=false');\n    }\n    const nextCredentials = normalizeXiaoyuzhouCredentials({\n        ...credentials,\n        access_token: parsed['x-jike-access-token'] || '',\n        refresh_token: parsed['x-jike-refresh-token'] || '',\n        expires_at: getNowMs() + XIAOYUZHOU_TOKEN_TTL_MS,\n    });\n    if (!nextCredentials.access_token || !nextCredentials.refresh_token) {\n        throw createXiaoyuzhouAuthError('Xiaoyuzhou refresh API returned empty access_token or refresh_token');\n    }\n    saveXiaoyuzhouCredentials(nextCredentials);\n    return nextCredentials;\n}\n\nfunction buildApiUrl(endpoint, query) {","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/auth.js#L137-L173","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: raw error with no body context\n// Failed to refresh -> Xiaoyuzhou refresh returned invalid JSON: Unexpected token '<'...\n// after: capture the body to diagnose\ntry {\n  await refreshXiaoyuzhouCredentials(creds);\n} catch (e) {\n  console.error('refresh failed:', e.message);\n  const probe = await fetch('https://api.xiaoyuzhoufm.com/app_auth_tokens.refresh', { method: 'HEAD' });\n  console.error('status:', probe.status, 'content-type:', probe.headers.get('content-type'));\n}","handlingStrategy":"try-catch","validationCode":"// probe the endpoint and confirm the response is JSON before refreshing\nconst probe = await fetch('https://api.xiaoyuzhoufm.com/app_auth_tokens.refresh', { method: 'OPTIONS' }).catch(() => null);\nconst contentType = probe?.headers?.get('content-type') ?? '';\nif (probe && !contentType.includes('application/json')) {\n  throw new Error(`Refresh endpoint answered with content-type '${contentType}' — a proxy/WAF or captive portal is likely intercepting traffic.`);\n}","typeGuard":"function isRefreshPayload(value) {\n  return typeof value === 'object' && value !== null\n    && typeof value.success === 'boolean'\n    && (value['x-jike-access-token'] === undefined || typeof value['x-jike-access-token'] === 'string');\n}","tryCatchPattern":"import { CommandExecutionError } from '@jackwener/opencli/errors';\ntry {\n  creds = await refreshXiaoyuzhouCredentials(creds);\n} catch (err) {\n  if (err instanceof CommandExecutionError && err.message.includes('refresh returned invalid JSON')) {\n    console.error('Non-JSON body from refresh endpoint — inspect for proxy/WAF/captive-portal interception.');\n    // capture body for diagnosis before failing\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["json","network","proxy","response-parsing"],"backgroundTag":"invalid-json-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}