DIYgod/RSSHub · warning · Error
Bilibili browser mode returned non-JSON response with status
Error message
Bilibili browser mode returned non-JSON response with status ${response.status()}; BILIBILI_COOKIE_* may be required What it means
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.
Source
Thrown at lib/routes/bilibili/video.ts:116
throw new Error(`Bilibili browser mode did not receive a video list response within ${browserResponseTimeout}ms: ${getErrorMessage(videoListResponseResult.error)}`);
}
return videoListResponseResult.response;
};
const waitForVideoListResponseFromVideoPage = async (page: Page, videoUrl: string): Promise<BrowserResponse> => {
const videoListResponsePromise = waitForVideoListResponse(page);
await navigateToVideoPage(page, videoUrl);
const response = await getVideoListResponse(videoListResponsePromise);
if (response.status() !== 200) {
throw new Error(`Bilibili browser mode returned unexpected video list API status ${response.status()}`);
}
const contentType = response.headers()['content-type'];
if (!contentType?.includes('application/json')) {
throw new Error(`Bilibili browser mode returned non-JSON response with status ${response.status()}; BILIBILI_COOKIE_* may be required`);
}
return response;
};
async function applyCookie(page: Page, cookie: string) {
const cookies = cookie
.split(';')
.map((item) => item.trim())
.filter(Boolean)
.map((item) => {
const equalIndex = item.indexOf('=');
if (equalIndex <= 0) {
return;
}
return {
name: item.slice(0, equalIndex).trim(),View on GitHub (pinned to bed535e087)
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.
Example fix
// before
const contentType = response.headers()['content-type'];
if (!contentType?.includes('application/json')) {
throw new Error(`Bilibili browser mode returned non-JSON response with status ${response.status()}; BILIBILI_COOKIE_* may be required`);
}
// after (log a snippet to confirm login-page vs captcha)
const contentType = response.headers()['content-type'];
if (!contentType?.includes('application/json')) {
const body = await response.text();
logger.error(`[bilibili/video] non-JSON body (first 200 chars): ${body.slice(0, 200)}`);
throw new Error(`Bilibili browser mode returned non-JSON (${contentType}) with status ${response.status()}; BILIBILI_COOKIE_* may be required`);
} Defensive patterns
Strategy: validation
Validate before calling
// Verify a cookie is configured and currently valid before allowing the browser fallback.
import { config } from '@/config';
import ofetch from '@/utils/ofetch';
async function hasUsableBilibiliCookie() {
const cookies = Object.values(config.bilibili.cookies).filter(Boolean);
if (cookies.length === 0) return false;
const nav = await ofetch<{ code: number }>('https://api.bilibili.com/x/web-interface/nav', { headers: { Cookie: cookies[0]! } });
return nav.code === 0;
} Type guard
function isJsonResponse(headers: Record<string, string>): boolean {
return (headers['content-type'] ?? '').includes('application/json');
} Try / catch
try {
if (!contentType?.includes('application/json')) {
throw new Error('non-JSON; cookie may be required');
}
} catch (e) {
if (/non-JSON|cookie/i.test(String(e))) {
// Refresh cookie and retry once; if still HTML, it's a captcha/challenge — give up.
await refreshBilibiliCookie();
await backoffRetry();
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Got error code ${data.code} while fetching in browser mode:
- Bilibili browser mode did not receive a video list response
- Bilibili browser mode returned unexpected video list API sta
- 缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值
- 对应 uid 的 Bilibili 用户的 Cookie 已过期
AI-assisted analysis of DIYgod/RSSHub@bed535e087 (2026-08-12).
Data as JSON: /api/errors/7969cf5575c490a5.
Report an issue: GitHub.