jackwener/OpenCLI · error · CommandExecutionError

`Malformed json3 timedtext response: ${err?.message || err}`

Error message

`Malformed json3 timedtext response: ${err?.message || err}`

What it means

parseJson3Segments parses YouTube's json3 timedtext format (the caption track format fetched from /api/timedtext?fmt=json3). If JSON.parse fails on the response body, this error wraps the underlying parse message. It signals the fetched caption body was not valid json3 JSON (truncated, HTML error page, or encoding issue).

Source

Thrown at clis/youtube/transcript.js:42

function normalizeSegmentsPayload(value, source, { allowNull = false } = {}) {
    const payload = unwrapBrowserResult(value);
    if (payload == null && allowNull)
        return null;
    if (Array.isArray(payload))
        return payload;
    if (payload && typeof payload === 'object' && payload.error) {
        throw new CommandExecutionError(String(payload.error));
    }
    throw new CommandExecutionError(`Malformed ${source} payload`);
}

function parseJson3Segments(text) {
    let data;
    try {
        data = JSON.parse(text);
    }
    catch (err) {
        throw new CommandExecutionError(`Malformed json3 timedtext response: ${err?.message || err}`);
    }
    if (!Array.isArray(data?.events)) {
        throw new CommandExecutionError('Malformed json3 timedtext response: missing events array');
    }
    const rows = [];
    for (const event of data.events) {
        const startMs = Number(event?.tStartMs || 0);
        const durMs = Number(event?.dDurationMs || 0);
        const segs = Array.isArray(event?.segs) ? event.segs : [];
        const line = segs.map(seg => seg?.utf8 || '').join('').replace(/\s+/g, ' ').trim();
        if (!line)
            continue;
        rows.push({
            start: startMs / 1000,
            end: (startMs + durMs) / 1000,
            text: line,
        });
    }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log the raw body prefix to see what was actually returned (HTML vs truncated JSON).
  2. Re-fetch captions with a fresh request — signed timedtext URLs expire and stale captures may hold dead responses.
  3. Ensure the browser session passed any consent/bot checks before capture (complete cookie consent in the automated tab).
  4. Fall back to the player captionTracks extraction path if network capture keeps yielding non-JSON bodies.

Example fix

// before
const segments = parseJson3Segments(body);
// after
let segments;
try {
  segments = parseJson3Segments(body);
} catch (err) {
  console.error('timedtext body head:', String(body).slice(0, 200));
  throw err;
}
Defensive patterns

Strategy: validation

Validate before calling

const body = typeof entry?.responsePreview === 'string' ? entry.responsePreview : '';
if (!body.trim().startsWith('{')) {
  // not JSON — skip before calling parseJson3Segments
}

Type guard

function looksLikeJson3(text) {
  if (typeof text !== 'string' || !text.trim().startsWith('{')) return false;
  try { const d = JSON.parse(text); return Array.isArray(d?.events); } catch { return false; }
}

Try / catch

try {
  const segments = parseJson3Segments(body);
} catch (err) {
  if (err.message.startsWith('Malformed json3 timedtext response')) {
    console.error('body head:', String(body).slice(0, 200));
  }
}

Prevention

When it happens

Trigger: `parsed` -> `parseJson3Segments(text)` where text is a captured or fetched timedtext body that is not valid JSON — e.g. an HTML error/consent page, empty string with garbage, or truncated network responsePreview.

Common situations: YouTube serving a bot-check or consent interstitial instead of caption data; network capture storing partial response bodies; proxies/firewalls rewriting responses; expired signed timedtext URLs returning error pages.

Understand the failure class

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/8aa0977932f8e287. Report an issue: GitHub.