unslothai/unsloth · error · TranscriptUnavailable

YouTube returned an unreadable response.

Error message

YouTube returned an unreadable response.

What it means

TranscriptUnavailable raised by _fetch_player when the innertube player response body cannot be decoded as UTF-8 or parsed as JSON (UnicodeDecodeError or ValueError from json.loads, chained via `from error`). HTTP status was OK and the body was under the size cap, but the content is not a JSON document.

Source

Thrown at studio/backend/core/youtube_transcript.py:188

                    "osName": "Android",
                    "osVersion": "11",
                    "hl": "en",
                    "gl": "US",
                },
            },
            "videoId": video_id,
            "contentCheckOk": True,
            "racyCheckOk": True,
        },
    ) as response:
        response.raise_for_status()
        body = await _read_capped(
            response, _MAX_PLAYER_BYTES, "YouTube returned an unexpectedly large response."
        )
    try:
        player = json.loads(body.decode("utf-8"))
    except (UnicodeDecodeError, ValueError) as error:
        raise TranscriptUnavailable("YouTube returned an unreadable response.") from error
    if not isinstance(player, dict):
        raise TranscriptUnavailable("YouTube returned an unreadable response.")
    return player


def _select_track(
    tracks: list[dict[str, Any]], tracklist: dict[str, Any], languages: Sequence[str]
) -> dict[str, Any]:
    for language in languages:
        wanted = str(language).strip().lower()
        if not wanted:
            continue
        base = wanted.split("-")[0]
        for want_generated in (False, True):
            candidates = [t for t in tracks if (t.get("kind") == "asr") is want_generated]
            # exact locale before the base-language fallback: a pt-BR request must not
            # take a pt-PT track just because it is listed first
            for matches_wanted in (

View on GitHub (pinned to 203007d190)

Solutions

  1. Retry the request after a delay or from a different network/IP.
  2. Inspect the raw response (log body[:200]) to identify consent/captcha pages; if consent-related, the request headers/client config need adjusting or cookies must be supplied.
  3. Rate-limit your fetching to avoid being served block pages.

Example fix

# before
player = await _fetch_player(client, video_id)  # raises on HTML body

# after
for attempt in range(3):
    try:
        player = await _fetch_player(client, video_id)
        break
    except TranscriptUnavailable:
        if attempt == 2:
            raise
        await asyncio.sleep(2 ** attempt)
Defensive patterns

Strategy: retry

Try / catch

for attempt in range(3):
    try:
        player_or_transcript = await fetch_transcript(video_id)
        break
    except TranscriptUnavailable as e:
        if "unreadable response" in str(e) and attempt < 2:
            await asyncio.sleep(2 ** attempt)
            continue
        raise

Prevention

When it happens

Trigger: YouTube (or an intercepting proxy) returns an HTML consent page, captcha, or error page with HTTP 200; a truncated or gzip-mangled body; a captive portal injecting content. Any of these make json.loads fail and hit this branch.

Common situations: Running transcript fetches from datacenter IPs that get soft-blocked into HTML interstitials; corporate proxies rewriting responses; transient CDN corruption.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/0947d6f00a0e869c. Report an issue: GitHub.