{"record":{"id":"177fb927096ea28d","repo":"jackwener/OpenCLI","slug":"malformed-caption-info-payload","errorCode":null,"errorMessage":"Malformed caption info payload","messagePattern":"Malformed caption info payload","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/youtube/transcript.js","lineNumber":456,"sourceCode":"          langPrefixMatched: !!(langPref && track.languageCode !== langPref && track.languageCode.startsWith(langPref))\n        };\n      })()\n    `));\n        if (!segments && (!captionData || typeof captionData !== 'object' || Array.isArray(captionData))) {\n            throw new CommandExecutionError(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'malformed response'}`);\n        }\n        if (captionData?.error) {\n            const msg = `${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`;\n            // \"No captions available\" 是合法 empty 数据条件（作者没开字幕 + YT 没自动生成），\n            // 与 bilibili subtitle 的 EmptyResultError 同模式。下游应按 code EMPTY_RESULT 跳过\n            // 重试和 softFail 计数。其它 error（HTTP / parse / 短暂空响应）仍按 fetch 失败抛。\n            if (captionData.error === 'No captions available for this video') {\n                throw new EmptyResultError('youtube transcript', '该视频没有字幕（作者未开启 + 无自动字幕）。');\n            }\n            throw new CommandExecutionError(msg);\n        }\n        if (!segments && typeof captionData?.captionUrl !== 'string') {\n            throw new CommandExecutionError('Malformed caption info payload');\n        }\n        // Warn if --lang was specified but not matched\n        if (captionData?.requestedLang && !captionData.langMatched && !captionData.langPrefixMatched) {\n            console.error(`Warning: --lang \"${captionData.requestedLang}\" not found. Using \"${captionData.language}\" instead. Available: ${captionData.available.join(', ')}`);\n        }\n        // Step 2: Fetch caption XML and parse segments\n        // Ensure caption URL requests srv3 XML format — YouTube may return empty\n        // responses when no explicit format is specified.\n        if (!segments) {\n            const originalCaptionUrl = captionData.captionUrl;\n            let captionUrl = originalCaptionUrl;\n            if (!/[&?]fmt=/.test(originalCaptionUrl)) {\n                captionUrl = originalCaptionUrl + (originalCaptionUrl.includes('?') ? '&' : '?') + 'fmt=srv3';\n            }\n            segments = normalizeSegmentsPayload(await page.evaluate(`\n      (async () => {\n        async function fetchCaptionXml(url) {\n          const resp = await fetch(url);","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/youtube/transcript.js#L438-L474","documentation":"After passing the shape and error checks, if no segments were already extracted and captionData.captionUrl is not a string, the CLI throws this fixed message. The caption info object was structurally valid but lacked the URL needed to fetch the caption XML (Step 2), so the payload is incomplete.","triggerScenarios":"`segments` where the caption-info script returned a well-formed object with captionTracks metadata but no captionUrl (and the earlier player/capture extraction paths yielded no segments).","commonSituations":"YouTube changes to how caption track baseUrl is exposed; partial player responses in SPA-navigated tabs; version skew between the CLI's injected script and current YouTube internals.","solutions":["Update the CLI so the caption-info script reads the current caption track URL field.","Check captionData keys (log them) to see where the URL now lives in YouTube's response.","Retry with a fresh page load so the player response is complete.","Use the network-capture fallback (timedtext requests) instead of the captionUrl path."],"exampleFix":"// before\nif (!captionData?.captionUrl) throw new Error('no url');\n// after\nif (!captionData?.captionUrl) {\n  console.error('caption info keys:', Object.keys(captionData));\n  throw new Error('Malformed caption info payload');\n}","handlingStrategy":"validation","validationCode":"if (captionData && typeof captionData === 'object' && typeof captionData.captionUrl !== 'string') {\n  console.error('caption info missing captionUrl, keys:', Object.keys(captionData));\n}","typeGuard":"function hasCaptionUrl(v) {\n  return !!v && typeof v === 'object' && typeof v.captionUrl === 'string' && v.captionUrl.length > 0;\n}","tryCatchPattern":"try {\n  // step 2: fetch caption XML via captionUrl\n} catch (err) {\n  if (err.message === 'Malformed caption info payload') {\n    console.error('caption info lacked captionUrl — schema drift likely');\n  }\n}","preventionTips":["Keep the CLI in sync with how YouTube exposes caption track baseUrls","Log captionData keys when the URL is missing to spot schema drift","Retry after a full page reload so the player response is complete","Prefer the network-capture path when captionUrl is absent"],"tags":["youtube","schema-validation","malformed-payload","browser-automation"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}