{"record":{"id":"e8a375b7ef3b8a29","repo":"unslothai/unsloth","slug":"youtube-returned-no-caption-text-for-this-video","errorCode":null,"errorMessage":"YouTube returned no caption text for this video.","messagePattern":"YouTube returned no caption text for this video\\.","errorType":"exception","errorClass":"TranscriptUnavailable","httpStatus":null,"severity":"warning","filePath":"studio/backend/core/youtube_transcript.py","lineNumber":278,"sourceCode":"        # Redirects are followed by hand so the host allowlist covers every hop, not\n        # just the URL the player response handed us.\n        async with client.stream(\n            \"GET\", url, headers = {\"User-Agent\": _USER_AGENT}, follow_redirects = False\n        ) as response:\n            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\")","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/youtube_transcript.py#L260-L296","documentation":"TranscriptUnavailable raised after the caption fetch completes when the response body is empty (falsy `body`). The request succeeded at HTTP level (raise_for_status passed) and redirects settled, but zero bytes of caption payload came back.","triggerScenarios":"Caption endpoint returning 200 with an empty body for a given fmt=json3 request; track exists but serves no data; transient server-side empty responses.","commonSituations":"Rare server-side glitches; requesting captions for tracks that were just disabled; rate-limited endpoints returning empty 200s.","solutions":["Retry the fetch after a short backoff.","Request a different track/language (the selected track may be a dead entry).","Fall back to local transcription if YouTube keeps returning empty bodies."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    transcript = await fetch_transcript(video_id)\nexcept TranscriptUnavailable as e:\n    if \"no caption text\" in str(e):\n        transcript = await fetch_transcript(video_id, languages=(\"en\",))  # other track, then give up\n    else:\n        raise","preventionTips":["Treat an empty 200 as retryable; do not mark the video permanently bad on first occurrence.","Select a specific language track on retry to bypass dead track entries.","Throttle requests — rate limiting sometimes manifests as empty 200 bodies."],"tags":["youtube","transcript","empty-response","retry"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}