{"record":{"id":"3526fc7e28fbc6ca","repo":"jackwener/OpenCLI","slug":"failed-to-reach-xiaoyuzhou-api-geterrormessage","errorCode":null,"errorMessage":"Failed to reach Xiaoyuzhou API: ${getErrorMessage(error)}","messagePattern":"Failed to reach Xiaoyuzhou API: (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/auth.js","lineNumber":204,"sourceCode":"    const {\n        method = 'GET',\n        query,\n        body,\n    } = options;\n    let response;\n    try {\n        response = await fetchImpl(buildApiUrl(endpoint, query), {\n            method,\n            headers: buildXiaoyuzhouHeaders(credentials, {\n                contentType: 'application/json',\n                includeLocalTime: true,\n            }),\n            body: body === undefined ? undefined : JSON.stringify(body),\n            signal: AbortSignal.timeout(20_000),\n        });\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Failed to reach Xiaoyuzhou API: ${getErrorMessage(error)}`);\n    }\n    return response;\n}\n\nexport async function requestXiaoyuzhouJson(endpoint, options = {}, fetchImpl = fetch) {\n    let credentials = options.credentials ?? loadXiaoyuzhouCredentials();\n    if (shouldRefreshXiaoyuzhouCredentials(credentials)) {\n        credentials = await refreshXiaoyuzhouCredentials(credentials, fetchImpl);\n    }\n    let response = await performXiaoyuzhouJsonRequest(endpoint, options, credentials, fetchImpl);\n    if (response.status === 401) {\n        credentials = await refreshXiaoyuzhouCredentials(credentials, fetchImpl);\n        response = await performXiaoyuzhouJsonRequest(endpoint, options, credentials, fetchImpl);\n    }\n    const bodyText = await response.text();\n    if (!response.ok) {\n        if (response.status === 401 || response.status === 403) {\n            throw createXiaoyuzhouAuthError(`Xiaoyuzhou API rejected the credentials with HTTP ${response.status}`);","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/auth.js#L186-L222","documentation":"Thrown by performXiaoyuzhouJsonRequest when the fetch call to a Xiaoyuzhou API endpoint throws before an HTTP response exists — network unreachable, DNS failure, TLS error, or the 20-second AbortSignal.timeout firing. It is wrapped as CommandExecutionError; auth problems are intentionally NOT this error (those surface later as AUTH_REQUIRED CliErrors).","triggerScenarios":"Any requestXiaoyuzhouJson call (episodes, transcripts, history, progress) where the TCP/TLS connection to api.xiaoyuzhoufm.com cannot be established or the request exceeds 20s: offline machine, DNS outage, firewall, or hung connection.","commonSituations":"No internet / Wi-Fi dropped; DNS misconfiguration; IPv6-only or broken dual-stack network; firewalled corporate network; slow VPN causing the 20s timeout; Node < 18 lacking global fetch (fetchImpl undefined → TypeError wrapped here).","solutions":["Verify reachability: curl -v https://api.xiaoyuzhoufm.com — fix connectivity, DNS, or firewall before retrying.","If timeouts recur, pass a custom fetchImpl with a longer AbortSignal timeout.","Add retry with exponential backoff for transient network errors around requestXiaoyuzhouJson calls.","Confirm Node >= 18 so global fetch is available."],"exampleFix":"// before: single attempt, hard failure on flaky network\nconst { data } = await requestXiaoyuzhouJson('/episodes/get', { query: { eid } });\n// after: bounded retry on transport errors\nasync function fetchWithRetry(endpoint, options) {\n  for (let attempt = 0; ; attempt++) {\n    try { return await requestXiaoyuzhouJson(endpoint, options); }\n    catch (e) {\n      if (!String(e.message).includes('Failed to reach') || attempt >= 2) throw e;\n      await new Promise(r => setTimeout(r, 500 * 2 ** attempt));\n    }\n  }\n}","handlingStrategy":"retry","validationCode":"// network preflight\nasync function assertApiReachable() {\n  try {\n    await fetch('https://api.xiaoyuzhoufm.com', { method: 'HEAD', signal: AbortSignal.timeout(5000) });\n  } catch (e) {\n    throw new Error(`Xiaoyuzhou API unreachable (${e.message}). Check network, DNS, proxy, and Node >= 18.`);\n  }\n}","typeGuard":"function isTransportError(err) {\n  return err instanceof Error && err.message.startsWith('Failed to reach Xiaoyuzhou API:');\n}","tryCatchPattern":"import { CommandExecutionError } from '@jackwener/opencli/errors';\nasync function withRetry(endpoint, options, attempts = 3) {\n  for (let i = 0; ; i++) {\n    try {\n      return await requestXiaoyuzhouJson(endpoint, options);\n    } catch (err) {\n      const transient = err instanceof CommandExecutionError\n        && err.message.startsWith('Failed to reach Xiaoyuzhou API:');\n      if (!transient || i >= attempts - 1) throw err;\n      await new Promise(r => setTimeout(r, 500 * 2 ** i));\n    }\n  }\n}","preventionTips":["Add exponential-backoff retry around all requestXiaoyuzhouJson calls for transient transport errors.","Monitor DNS/connectivity in the deployment environment; allowlist api.xiaoyuzhoufm.com in firewalls.","Use Node >= 18 so global fetch and AbortSignal.timeout are available.","Prefer failing fast with a clear preflight reachability check over long hangs at the 20s timeout."],"tags":["network","timeout","dns","fetch"],"backgroundTag":"network-request-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}