{"record":{"id":"a4e321bfe85c2ac2","repo":"unslothai/unsloth","slug":"youtube-returned-unreadable-caption-data","errorCode":null,"errorMessage":"YouTube returned unreadable caption data.","messagePattern":"YouTube returned unreadable caption data\\.","errorType":"exception","errorClass":"TranscriptUnavailable","httpStatus":null,"severity":"error","filePath":"studio/backend/core/youtube_transcript.py","lineNumber":283,"sourceCode":"            location = response.headers.get(\"location\")\n            if response.is_redirect and location:\n                url = urljoin(url, location)\n                _validated_caption_url(url)\n                continue\n            response.raise_for_status()\n            body = await _read_capped(\n                response, _MAX_CAPTION_BYTES, \"This video's captions are too large to attach.\"\n            )\n        break\n    else:\n        raise TranscriptUnavailable(\"YouTube redirected the caption request too many times.\")\n    if not body:\n        raise TranscriptUnavailable(\"YouTube returned no caption text for this video.\")\n\n    try:\n        payload = json.loads(body.decode(\"utf-8\"))\n    except (UnicodeDecodeError, ValueError) as error:\n        raise TranscriptUnavailable(\"YouTube returned unreadable caption data.\") from error\n    events = payload.get(\"events\") if isinstance(payload, dict) else None\n    return _flatten_events(events or [])\n\n\ndef _flatten_events(events: list[Any]) -> str:\n    lines: list[str] = []\n    for event in events:\n        if not isinstance(event, dict):\n            continue\n        # aAppend cues carry only the rolling-window newline between ASR lines.\n        if event.get(\"aAppend\") == 1:\n            continue\n        segments = event.get(\"segs\")\n        if not isinstance(segments, list):\n            continue\n        joined = \"\".join(\n            str(segment.get(\"utf8\") or \"\") for segment in segments if isinstance(segment, dict)\n        )","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/youtube_transcript.py#L265-L301","documentation":"TranscriptUnavailable raised when the caption response body cannot be decoded as UTF-8 or parsed as JSON. Analogous to error 789 but for the caption json3 payload: bytes were received and non-empty, but json.loads fails (UnicodeDecodeError/ValueError, chained from the original error).","triggerScenarios":"Caption endpoint returning HTML (error/consent page) or binary/garbage content instead of json3; truncated bodies; encoding mismatches. Distinct from 793 (empty body) — here the body is present but unparseable.","commonSituations":"Soft-blocks or interstitials served on the caption URL; transient payload corruption; proxy interference.","solutions":["Retry with backoff or a different egress IP.","Log body[:200] to identify what was actually returned; HTML indicates blocking/consent issues rather than a parse bug.","Slow down request rates to avoid triggering block pages."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for attempt in range(2):\n    try:\n        transcript = await fetch_transcript(video_id)\n        break\n    except TranscriptUnavailable as e:\n        if \"unreadable caption data\" in str(e) and attempt == 0:\n            continue\n        raise","preventionTips":["Retry unparseable caption payloads once before failing the video.","Log a body snippet on failure to distinguish block pages from corruption.","Avoid aggressive parallel fetching that triggers soft-blocks on caption URLs."],"tags":["youtube","transcript","json-parse","network"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}