yt-dlp/yt-dlp · error · ExtractorError

{code}: {error_id}: {message}

Error message

{code}: {error_id}: {message}

What it means

Thrown by StreaksBaseIE as a catch-all for STREAKS API errors: HTTP 403/404 whose JSON body carries any 'code' or 'message' other than the special-cased REQUEST_FAILED(124/126) and MEDIA_NOT_FOUND. The message is built with join_nonempty(code, error_id, message), so it reads like 'RATE_LIMITED: 77: too many requests'. Not marked expected, so yt-dlp treats it as an extraction failure.

Source

Thrown at yt_dlp/extractor/streaks.py:56

    def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, live_from_start=False):
        try:
            response = self._download_streaks_playback_json(project_id, media_id, headers=headers)
        except ExtractorError as e:
            if isinstance(e.cause, HTTPError) and e.cause.status in (403, 404):
                error = self._parse_json(e.cause.response.read().decode(), media_id, fatal=False)
                message = traverse_obj(error, ('message', {clean_html}, filter))
                code = traverse_obj(error, ('code', {clean_html}, filter))
                error_id = traverse_obj(error, ('id', {int_or_none}))
                if code == 'REQUEST_FAILED':
                    if error_id == 124:
                        self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
                    elif error_id == 126:
                        raise ExtractorError('Access is denied (possibly due to invalid/missing API key)')
                if code == 'MEDIA_NOT_FOUND':
                    raise ExtractorError(join_nonempty(code, message, delim=': '), expected=True)
                if code or message:
                    raise ExtractorError(join_nonempty(code, error_id, message, delim=': '))
            raise

        streaks_id = response['id']
        live_status = {
            'clip': 'was_live',
            'file': 'not_live',
            'linear': 'is_live',
            'live': 'is_live',
        }.get(response.get('type'))

        formats, subtitles = [], {}
        drm_formats = False
        sources = response['sources']
        ssai = traverse_obj(sources, (..., 'ssai', {dict}, any))

        for source in traverse_obj(sources, (
            lambda _, v: url_or_none(v['src']),
        )):

View on GitHub (pinned to 81ecd58b13)

Solutions

  1. Re-run with yt_dlp -v to see the full message; the embedded code/id/message identifies the backend reason.
  2. If it looks like throttling, wait and retry with slower rate (--limit-rate, sleep_interval) or fewer parallel fragment downloads.
  3. Update yt-dlp to latest — new codes get mapped to friendlier errors over time.
  4. If the message indicates auth/entitlement, supply cookies (--cookies-from-browser) or view the page in a browser to see what access it demands.
Defensive patterns

Strategy: retry

Try / catch

from yt_dlp.utils import ExtractorError
for attempt in range(3):
    try:
        ydl.extract_info(url)
        break
    except ExtractorError as e:
        if 'RATE_LIMIT' in str(e).upper():
            time.sleep(30 * (attempt + 1))
            continue
        raise

Prevention

When it happens

Trigger: The playback API returns an unmapped error code — rate limiting, expired token, bad request, project suspended — with 403/404 status and a JSON body containing 'code' and/or 'message'. Anything not matching the two special cases funnels here.

Common situations: Hammering the API from one IP triggers throttling codes; site adds a new error code the extractor has not mapped; entitlement/token codes when the media requires auth the caller did not send.

Related errors


AI-assisted analysis of yt-dlp/yt-dlp@81ecd58b13 (2026-08-22). Data as JSON: /api/errors/e32ac0b8b7a2bce7. Report an issue: GitHub.