{"record":{"id":"4da29d8c8afcb5ac","repo":"jackwener/OpenCLI","slug":"failed-to-refresh-xiaoyuzhou-credentials-geterr","errorCode":null,"errorMessage":"Failed to refresh Xiaoyuzhou credentials: ${getErrorMessage(error)}","messagePattern":"Failed to refresh Xiaoyuzhou credentials: (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/auth.js","lineNumber":144,"sourceCode":"\nexport async function refreshXiaoyuzhouCredentials(credentials, fetchImpl = fetch) {\n    if (!credentials.refresh_token) {\n        throw createXiaoyuzhouAuthError('Xiaoyuzhou refresh token is missing');\n    }\n    let response;\n    try {\n        response = await fetchImpl(`${XIAOYUZHOU_API_BASE_URL}/app_auth_tokens.refresh`, {\n            method: 'POST',\n            headers: buildXiaoyuzhouHeaders(credentials, {\n                contentType: 'application/x-www-form-urlencoded; charset=utf-8',\n                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'] || '',","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/auth.js#L126-L162","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: implicit global fetch with 20s timeout\nawait refreshXiaoyuzhouCredentials(creds);\n// after: retry with custom fetch and longer timeout on transient network errors\nconst fetchWithRetry = async (url, init) => {\n  for (let i = 0; i < 3; i++) {\n    try { return await fetch(url, { ...init, signal: AbortSignal.timeout(60_000) }); }\n    catch (e) { if (i === 2) throw e; await new Promise(r => setTimeout(r, 1000 * (i + 1))); }\n  }\n};\nawait refreshXiaoyuzhouCredentials(creds, fetchWithRetry);","handlingStrategy":"retry","validationCode":"// preflight connectivity check before making API calls\nconst reachable = await fetch('https://api.xiaoyuzhoufm.com', { method: 'HEAD', signal: AbortSignal.timeout(5000) })\n  .then(() => true)\n  .catch(() => false);\nif (!reachable) throw new Error('api.xiaoyuzhoufm.com is unreachable — check network/proxy/VPN before retrying.');","typeGuard":"function isTransportError(err) {\n  return err instanceof Error && err.message.startsWith('Failed to refresh Xiaoyuzhou credentials:');\n}","tryCatchPattern":"import { CommandExecutionError } from '@jackwener/opencli/errors';\ntry {\n  await refreshXiaoyuzhouCredentials(creds);\n} catch (err) {\n  if (err instanceof CommandExecutionError && err.message.startsWith('Failed to refresh Xiaoyuzhou credentials')) {\n    // transient network/timeout — retry with backoff\n    await new Promise(r => setTimeout(r, 2000));\n    return refreshXiaoyuzhouCredentials(creds);\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["network","token-refresh","timeout","fetch"],"backgroundTag":"network-request-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}