{"record":{"id":"dd8bd181d7315e1b","repo":"yt-dlp/yt-dlp","slug":"got-empty-response-from-playback-api","errorCode":null,"errorMessage":"Got empty response from playback API","messagePattern":"Got empty response from playback API","errorType":"exception","errorClass":"ExtractorError","httpStatus":null,"severity":"error","filePath":"yt_dlp/extractor/ondemandkorea.py","lineNumber":80,"sourceCode":"            'duration': 7267.0,\n            'title': 'The Outlaws: Main Movie',\n            'thumbnail': r're:^https?://.*\\.(jpg|jpeg|png)',\n            'age_limit': 18,\n        },\n    }, {\n        'url': 'https://www.ondemandkorea.com/en/player/vod/capture-the-moment-how-is-that-possible?contentId=1605006',\n        'only_matching': True,\n    }]\n\n    def _real_extract(self, url):\n        video_id = self._match_id(url)\n\n        data = self._download_json(\n            f'https://odkmedia.io/odx/api/v3/playback/{video_id}/', video_id, fatal=False,\n            headers={'service-name': 'odk'}, query={'did': str(uuid.uuid4())}, expected_status=(403, 404))\n        if not traverse_obj(data, ('result', {dict})):\n            msg = traverse_obj(data, ('messages', '__default'), 'title', expected_type=str)\n            raise ExtractorError(msg or 'Got empty response from playback API', expected=True)\n\n        data = data['result']\n\n        def try_geo_bypass(url):\n            return traverse_obj(url, ({parse_qs}, 'stream_url', 0, {url_or_none})) or url\n\n        formats = []\n        for m3u8_url in traverse_obj(data, (('sources', 'manifest'), ..., 'url', {url_or_none}, {try_geo_bypass})):\n            mod_url = re.sub(r'_720(p?)\\.m3u8', r'_1080\\1.m3u8', m3u8_url)\n            if mod_url != m3u8_url:\n                mod_format = self._extract_m3u8_formats(\n                    mod_url, video_id, note='Checking for higher quality format',\n                    errnote='No higher quality format found', fatal=False)\n                if mod_format:\n                    formats.extend(mod_format)\n                    continue\n            formats.extend(self._extract_m3u8_formats(m3u8_url, video_id, fatal=False))\n","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/yt-dlp/yt-dlp/blob/81ecd58b1394793e6da9998cc19fdb45657f1685/yt_dlp/extractor/ondemandkorea.py#L62-L98","documentation":"OnDemandKoreaIE calls odkmedia.io's v3 playback API with 403/404 whitelisted via expected_status, so HTTP errors come back as JSON. When the response has no 'result' object it surfaces the API's own message (messages.__default or title) or falls back to 'Got empty response from playback API', always with expected=True. The generic text appears only when the error envelope carries no readable message; the usual causes are regional licensing (403) or removed/wrong contentId (404).","triggerScenarios":"The playback API answers 403 (region not licensed for the title), 404 (removed or stale contentId), or 200 with an error envelope lacking messages.__default/title — the last case produces exactly the generic message.","commonSituations":"Viewers outside ODK's licensed regions without a matching egress IP; deep links with stale contentIds after catalog reshuffles; transient ODK API incidents.","solutions":["Read the full error line first — when present, the API's own message (messages.__default) states the real reason (region vs removed).","Route through an egress IP in a licensed region if you are entitled to the content.","Re-resolve the contentId from the current OnDemandKorea page URL; stale ids return empty results.","Confirm the title still exists on the site; if yes and it still fails on latest yt-dlp, report with -v."],"exampleFix":"# before — stale contentId / wrong region\nyt-dlp 'https://www.ondemandkorea.com/en/player/vod/some-title?contentId=1605006'\n\n# after — refresh contentId from the site and run from a licensed region\nyt-dlp --proxy 'http://us-proxy.example:8080' 'https://www.ondemandkorea.com/en/player/vod/some-title?contentId=<current-id>'","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"from yt_dlp.utils import DownloadError\n\ndef is_odk_playback_empty(e: BaseException) -> bool:\n    return isinstance(e, DownloadError) and 'Got empty response from playback API' in str(e)","tryCatchPattern":"from yt_dlp import YoutubeDL\nfrom yt_dlp.utils import DownloadError\n\ntry:\n    with YoutubeDL({'quiet': True}) as ydl:\n        ydl.extract_info(url, download=True)\nexcept DownloadError as e:\n    if 'Got empty response from playback API' in str(e):\n        refresh_content_id_and_egress(url)  # geo or removed: change egress / re-resolve id, retry once\n    else:\n        raise","preventionTips":["Run OnDemandKorea jobs from IPs in licensed regions.","Resolve contentIds fresh from the site URL before each download; never hard-code them long-term.","Keep the API-provided message in logs when present — it distinguishes geo blocks from removals."],"tags":["yt-dlp","ondemandkorea","geo-restricted","api-error","expected","extractor"],"backgroundTag":"geo-restricted-content","analyzedSha":"81ecd58b1394793e6da9998cc19fdb45657f1685","analyzedAt":"2026-08-22T12:21:25.439Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}