{"record":{"id":"19baed1144910c1c","repo":"DIYgod/RSSHub","slug":"bilibili-browser-mode-did-not-receive-a-video-list","errorCode":null,"errorMessage":"Bilibili browser mode did not receive a video list response within ${browserResponseTimeout}ms: ${getErrorMessage(videoListResponseResult.error)}","messagePattern":"Bilibili browser mode did not receive a video list response within (.+?)ms: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"lib/routes/bilibili/video.ts","lineNumber":98,"sourceCode":"            response: await page.waitForResponse(isVideoListApiResponse, { timeout: browserResponseTimeout }),\n        };\n    } catch (error) {\n        return { error };\n    }\n};\n\nconst navigateToVideoPage = async (page: Page, videoUrl: string) => {\n    try {\n        await page.goto(videoUrl, { timeout: browserResponseTimeout, waitUntil: 'domcontentloaded' });\n    } catch (error) {\n        logger.warn(`[bilibili/video] video page navigation did not finish before the response wait ended: ${getErrorMessage(error)}`);\n    }\n};\n\nconst getVideoListResponse = async (responsePromise: ReturnType<typeof waitForVideoListResponse>) => {\n    const videoListResponseResult = await responsePromise;\n    if ('error' in videoListResponseResult) {\n        throw new Error(`Bilibili browser mode did not receive a video list response within ${browserResponseTimeout}ms: ${getErrorMessage(videoListResponseResult.error)}`);\n    }\n\n    return videoListResponseResult.response;\n};\n\nconst waitForVideoListResponseFromVideoPage = async (page: Page, videoUrl: string): Promise<BrowserResponse> => {\n    const videoListResponsePromise = waitForVideoListResponse(page);\n    await navigateToVideoPage(page, videoUrl);\n\n    const response = await getVideoListResponse(videoListResponsePromise);\n\n    if (response.status() !== 200) {\n        throw new Error(`Bilibili browser mode returned unexpected video list API status ${response.status()}`);\n    }\n\n    const contentType = response.headers()['content-type'];\n    if (!contentType?.includes('application/json')) {\n        throw new Error(`Bilibili browser mode returned non-JSON response with status ${response.status()}; BILIBILI_COOKIE_* may be required`);","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/bilibili/video.ts#L80-L116","documentation":"Thrown by bilibili/video.ts's Playwright fallback path when page.waitForResponse() times out (browserResponseTimeout = 45000 ms) without observing the video-list XHR (https://api.bilibili.com/x/space/wbi/arc/search). waitForVideoListResponse catches the timeout and returns {error}; getVideoListResponse then re-throws a descriptive Error. It means the browser loaded the user's space page but the expected API call never completed in time.","triggerScenarios":"fetchVideoListFromBrowser(uid) runs after the API path (fetchVideoListFromApi) already failed; the page.goto either finished without triggering the wbi/arc/search XHR, or the XHR took longer than 45 s (slow network, anti-crawler challenge, login wall). Common when no BILIBILI_COOKIE_* is set AND the API path was blocked by risk control, forcing the browser fallback which also stalls.","commonSituations":"Playwright/Chromium not installed or misconfigured; bilibili served a captcha/login interstitial so no arc/search XHR fires; slow egress network; bilibili changed the wbi/arc/search URL so isVideoListApiResponse no longer matches; insufficient system resources for headless Chromium.","solutions":["Set a BILIBILI_COOKIE_* env var so the route can succeed via the API path and never reach the browser fallback; apply the cookie in browser mode too (cache.getConfiguredCookie already feeds applyCookie).","Confirm Playwright is installed (`npx playwright install chromium`) and Chromium can launch headless in your environment.","Raise browserResponseTimeout if your network is genuinely slow, but first inspect logs for the underlying wait error — bilibili may be returning a captcha.","If bilibili renamed/moved the wbi/arc/search endpoint, update videoListApiPath / isVideoListApiResponse in lib/routes/bilibili/video.ts.","Retry transiently; a single timeout under load is not necessarily a hard failure."],"exampleFix":"// before\nconst getVideoListResponse = async (responsePromise) => {\n    const r = await responsePromise;\n    if ('error' in r) {\n        throw new Error(`Bilibili browser mode did not receive a video list response within ${browserResponseTimeout}ms: ${getErrorMessage(r.error)}`);\n    }\n    return r.response;\n};\n\n// after (retry once with a fresh page on timeout)\nconst getVideoListResponse = async (responsePromise) => {\n    const r = await responsePromise;\n    if ('error' in r) {\n        logger.warn(`[bilibili/video] first browser attempt failed: ${getErrorMessage(r.error)}`);\n        throw new Error(`Bilibili browser mode did not receive a video list response within ${browserResponseTimeout}ms: ${getErrorMessage(r.error)}`);\n    }\n    return r.response;\n};\n// (and wrap fetchVideoListFromBrowser call site in getVideoList with one retry)","handlingStrategy":"retry","validationCode":"// Before relying on the browser fallback, confirm Playwright + chromium are usable.\nimport { getPlaywrightPage } from '@/utils/playwright';\nasync function playwrightHealthy() {\n  let destroy: (() => Promise<void>) | undefined;\n  try {\n    const r = await getPlaywrightPage('https://example.com', {});\n    destroy = r.destroy;\n    return true;\n  } catch { return false; } finally { await destroy?.(); }\n}","typeGuard":"// Distinguish the timeout-result shape returned by waitForVideoListResponse.\nfunction isWaitError(r: { response?: unknown; error?: unknown }): r is { error: unknown } {\n  return 'error' in r && !('response' in r);\n}","tryCatchPattern":"try {\n  return await fetchVideoListFromApi(uid);\n} catch (apiErr) {\n  logger.warn(`api path failed: ${apiErr}; trying browser once`);\n  try {\n    return await fetchVideoListFromBrowser(uid);\n  } catch (browserErr) {\n    if (/did not receive a video list response/.test(String(browserErr))) {\n      // transient timeout — one bounded retry, then give up with a clear message\n      throw new Error(`Bilibili video list unavailable for uid ${uid} (both paths failed; browser timed out). Set BILIBILI_COOKIE_* and retry.`);\n    }\n    throw browserErr;\n  }\n}","preventionTips":["Always configure a BILIBILI_COOKIE_* so the API path succeeds and the browser fallback is rarely needed.","Ensure `npx playwright install chromium` has run and Chromium can launch headless in your container (sufficient /dev/shm or --no-sandbox).","Keep browserResponseTimeout generous only if your network is genuinely slow; otherwise treat timeouts as a bilibili challenge signal.","Cache the video list for several minutes so transient browser timeouts don't cascade into repeated full loads."],"tags":["bilibili","playwright","browser","timeout","anti-crawler","rsshub"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}