{"record":{"id":"c888e91e10cac1e7","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-transcript-download-failed-with-http","errorCode":null,"errorMessage":"Xiaoyuzhou transcript download failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}","messagePattern":"Xiaoyuzhou transcript download failed with HTTP (.+?)(.+?)` : ''\\}","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/auth.js","lineNumber":276,"sourceCode":"export async function fetchXiaoyuzhouTranscriptBody(url, fetchImpl = fetch) {\n    let response;\n    try {\n        response = await fetchImpl(url, {\n            method: 'GET',\n            headers: {\n                'User-Agent': XIAOYUZHOU_DEFAULT_USER_AGENT,\n                Accept: '*/*',\n                Market: 'AppStore',\n            },\n            signal: AbortSignal.timeout(20_000),\n        });\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Failed to fetch Xiaoyuzhou transcript content: ${getErrorMessage(error)}`);\n    }\n    const bodyText = await response.text();\n    if (!response.ok) {\n        throw new CommandExecutionError(`Xiaoyuzhou transcript download failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}`);\n    }\n    return bodyText;\n}\n\nexport function extractTranscriptText(transcriptBody) {\n    let parsed;\n    try {\n        parsed = JSON.parse(transcriptBody);\n    }\n    catch {\n        return { text: '', segmentCount: 0 };\n    }\n    let items = [];\n    if (Array.isArray(parsed)) {\n        items = parsed;\n    }\n    else if (parsed && typeof parsed === 'object') {\n        for (const key of ['segments', 'data', 'transcript', 'items']) {","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/auth.js#L258-L294","documentation":"Thrown by fetchXiaoyuzhouTranscriptBody (clis/xiaoyuzhou/auth.js:276) when the transcript endpoint responded with a non-2xx HTTP status. The library reads the response body and appends it to the message so the server's error payload (rate-limit notice, 403 anti-bot page, 404) is visible. Unlike error 4900, the network round-trip succeeded — the server actively rejected the request.","triggerScenarios":"fetchXiaoyuzhouTranscriptBody(url) returns response.ok === false — e.g. HTTP 403 (CDN/WAF blocking the default User-Agent or the AppStore Market header being rejected), 404 (episode transcript no longer exists / wrong URL), 429 (rate limited), or 5xx (server outage).","commonSituations":"Scraping many transcripts quickly and hitting rate limits; Xiaoyuzhou changing their CDN rules so the hardcoded User-Agent ('Xiaoyuzhou Default User Agent'/AppStore Market) is blocked; passing an episode ID whose transcript was removed, yielding a 404; transient 502/503 during server incidents.","solutions":["Read the HTTP status and body appended to the message to determine server-side cause","For 403, update the User-Agent / Market headers in auth.js:263-266 to match the current official app values","For 429, add backoff/delay between transcript requests and retry later","For 404, verify the episode ID/URL is correct and that a transcript actually exists for it","For 5xx, retry after a delay — it is a server-side outage"],"exampleFix":"// before\nconst body = await fetchXiaoyuzhouTranscriptBody(url);\n// after\ntry {\n  const body = await fetchXiaoyuzhouTranscriptBody(url);\n} catch (error) {\n  const m = /HTTP (\\d{3})/.exec(error.message);\n  if (m && m[1] === '429') {\n    await new Promise(r => setTimeout(r, 5000));\n    return fetchXiaoyuzhouTranscriptBody(url);\n  }\n  throw error;\n}","handlingStrategy":"retry","validationCode":"const probe = await fetch(url, { method: 'HEAD' });\nif (!probe.ok) console.warn(`Endpoint preflight returned HTTP ${probe.status}; expect failure.`);","typeGuard":"null","tryCatchPattern":"try {\n  const body = await fetchXiaoyuzhouTranscriptBody(url);\n} catch (error) {\n  const status = Number(/HTTP (\\d{3})/.exec(error.message)?.[1] ?? 0);\n  if (status === 429 || status >= 500) { /* wait and retry */ }\n  else if (status === 403) { /* fix headers/User-Agent */ }\n  else throw error; // 404 etc. is permanent\n}","preventionTips":["Throttle transcript requests to avoid 429 rate limits","Keep User-Agent/Market headers in auth.js current with the official app","Validate episode URLs before fetching to avoid 404s","Monitor the appended response body in the message for server-side hints"],"tags":["http","http-status","rate-limit","cli"],"backgroundTag":"http-non-2xx-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}