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

  1. Set BILIBILI_COOKIE_{uid} (full Cookie from a logged-in bilibili.com session); applyCookie will inject it into the browser context before navigation.
  2. If a cookie is already set, refresh it — SESSDATA may have expired.
  3. 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.
  4. 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

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


AI-assisted analysis of DIYgod/RSSHub@bed535e087 (2026-08-12). Data as JSON: /api/errors/7969cf5575c490a5. Report an issue: GitHub.