{"record":{"id":"7969cf5575c490a5","repo":"DIYgod/RSSHub","slug":"bilibili-browser-mode-returned-non-json-response-w","errorCode":null,"errorMessage":"Bilibili browser mode returned non-JSON response with status ${response.status()}; BILIBILI_COOKIE_* may be required","messagePattern":"Bilibili browser mode returned non-JSON response with status (.+?); BILIBILI_COOKIE_\\* may be required","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"lib/routes/bilibili/video.ts","lineNumber":116,"sourceCode":"        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`);\n    }\n\n    return response;\n};\n\nasync function applyCookie(page: Page, cookie: string) {\n    const cookies = cookie\n        .split(';')\n        .map((item) => item.trim())\n        .filter(Boolean)\n        .map((item) => {\n            const equalIndex = item.indexOf('=');\n            if (equalIndex <= 0) {\n                return;\n            }\n\n            return {\n                name: item.slice(0, equalIndex).trim(),","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/bilibili/video.ts#L98-L134","documentation":"Thrown by waitForVideoListResponseFromVideoPage when the arc/search XHR returned HTTP 200 but the Content-Type header does not include 'application/json' — meaning bilibili shipped an HTML body (login page, captcha, or anti-bot challenge) instead of the expected JSON. The error message explicitly hints that BILIBILI_COOKIE_* may be required, because the most common cause is an unauthenticated browser session that bilibili redirects to a login HTML page answered with status 200.","triggerScenarios":"fetchVideoListFromBrowser(uid) with no (or expired) BILIBILI_COOKIE_*: the XHR technically succeeds with status 200 but the body is the HTML login/captcha page, so Content-Type is text/html. Also happens when bilibili serves an anti-bot JS-challenge page.","commonSituations":"Operator relies on the browser fallback without configuring any BILIBILI_COOKIE_*; cookie expired so the session fell back to anonymous; bilibili's anti-bot returned a challenge page under status 200.","solutions":["Set BILIBILI_COOKIE_{uid} (full Cookie from a logged-in bilibili.com session); applyCookie will inject it into the browser context before navigation.","If a cookie is already set, refresh it — SESSDATA may have expired.","Confirm the response really is a login page by temporarily logging response.text() in the handler; if it is a captcha, slow down or rotate IP.","Route RSSHub through a non-flagged IP if bilibili's anti-bot is serving challenge HTML."],"exampleFix":"// before\nconst contentType = response.headers()['content-type'];\nif (!contentType?.includes('application/json')) {\n    throw new Error(`Bilibili browser mode returned non-JSON response with status ${response.status()}; BILIBILI_COOKIE_* may be required`);\n}\n\n// after (log a snippet to confirm login-page vs captcha)\nconst contentType = response.headers()['content-type'];\nif (!contentType?.includes('application/json')) {\n    const body = await response.text();\n    logger.error(`[bilibili/video] non-JSON body (first 200 chars): ${body.slice(0, 200)}`);\n    throw new Error(`Bilibili browser mode returned non-JSON (${contentType}) with status ${response.status()}; BILIBILI_COOKIE_* may be required`);\n}","handlingStrategy":"validation","validationCode":"// Verify a cookie is configured and currently valid before allowing the browser fallback.\nimport { config } from '@/config';\nimport ofetch from '@/utils/ofetch';\nasync function hasUsableBilibiliCookie() {\n  const cookies = Object.values(config.bilibili.cookies).filter(Boolean);\n  if (cookies.length === 0) return false;\n  const nav = await ofetch<{ code: number }>('https://api.bilibili.com/x/web-interface/nav', { headers: { Cookie: cookies[0]! } });\n  return nav.code === 0;\n}","typeGuard":"function isJsonResponse(headers: Record<string, string>): boolean {\n  return (headers['content-type'] ?? '').includes('application/json');\n}","tryCatchPattern":"try {\n  if (!contentType?.includes('application/json')) {\n    throw new Error('non-JSON; cookie may be required');\n  }\n} catch (e) {\n  if (/non-JSON|cookie/i.test(String(e))) {\n    // Refresh cookie and retry once; if still HTML, it's a captcha/challenge — give up.\n    await refreshBilibiliCookie();\n    await backoffRetry();\n  }\n  throw e;\n}","preventionTips":["Always set at least one BILIBILI_COOKIE_* — applyCookie only runs when getConfiguredCookie() returns a value.","Periodically validate the cookie via /x/web-interface/nav; refresh when -101 appears.","Log response.text() (first N chars) on the non-JSON path to distinguish login-page from captcha challenge.","Run RSSHub behind a residential/clean IP to avoid bilibili serving anti-bot HTML."],"tags":["bilibili","playwright","browser","authentication","anti-crawler","cookie","rsshub"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}