jackwener/OpenCLI · error · LoginWallError
Server returned HTML instead of JSON (status=${value.status}
Error message
Server returned HTML instead of JSON (status=${value.status}). Likely a login wall, rate limit, or WAF challenge. What it means
throwIfLoginWall inspects a value fetched from a web endpoint and throws LoginWallError (code LOGIN_WALL, exit 77 NOPERM) when the payload looks like an HTML page rather than JSON. The library does this so a login wall / rate-limit page / WAF challenge surfaces as a typed error with status, URL, and a 100-char body preview instead of a cryptic JSON.parse SyntaxError.
Source
Thrown at src/utils.ts:116
}
function isLoginWallSignal(v: unknown): v is LoginWallSignal {
return (
typeof v === 'object'
&& v !== null
&& (v as Record<string, unknown>).__loginWall === true
&& typeof (v as Record<string, unknown>).status === 'number'
);
}
/** Throw a `LoginWallError` if `value` is the sentinel returned by the
* browser-side sniffer; otherwise return `value` unchanged. Adapters that
* fetch from inside `page.evaluate` call this on the result before consuming
* it, so the Node-side gets a typed error instead of a JSON-parse stack
* trace. */
export function throwIfLoginWall<T>(value: T, opts: { url?: string } = {}): T {
if (isLoginWallSignal(value)) {
throw new LoginWallError(
`Server returned HTML instead of JSON (status=${value.status}). `
+ `Likely a login wall, rate limit, or WAF challenge.`,
value.status,
opts.url || value.url || '',
value.bodyPreview,
);
}
return value;
}
/** Parse a `Response` body as JSON, throwing `LoginWallError` if the server
* returned an HTML page (login wall / rate limit / WAF interception) instead
* of the expected JSON. Catches the common case of `<!DOCTYPE` or `<html`
* leading the body \u2014 naive `JSON.parse` on these gives a cryptic
* `SyntaxError` that callers can't distinguish from "real" malformed JSON.
*
* On real (non-HTML) JSON-parse failures, throws a regular `Error` with a
* body preview attached so debugging doesn't require a packet capture. */View on GitHub (pinned to 49907e53dc)
Solutions
- Re-authenticate in the browser (the CLI relies on your logged-in browser session), then retry
- Wait a few minutes if rate-limited, then retry with backoff
- Open the error's url in a real browser to confirm whether it's a login page or WAF challenge
- Check err.status/err.bodyPreview on the LoginWallError to determine which case applies
Example fix
// before
const data = JSON.parse(await page.evaluate(fetchJson, url));
// after
const raw = await page.evaluate(fetchJson, url);
const data = throwIfLoginWall(JSON.parse(raw), { url }); Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: probe the endpoint once and check content-type before batch work
const probe = await fetch(url);
if ((probe.headers.get('content-type') || '').includes('text/html')) throw new Error('login wall detected before start'); Type guard
function isLoginWallError(e: unknown): e is LoginWallError {
return e instanceof LoginWallError;
} Try / catch
try {
const data = throwIfLoginWall(raw, { url });
} catch (e) {
if (e instanceof LoginWallError) {
console.error(`Walled at ${e.url} (status ${e.status}): ${e.bodyPreview}. Re-login or back off.`);
} else throw e;
} Prevention
- Keep the browser profile's site session fresh; re-login when cookies expire
- Throttle request rates to avoid WAF/rate-limit interstitials
- Check content-type before JSON.parse in your own fetch wrappers
- Route automation through your daily logged-in browser profile
When it happens
Trigger: Any adapter fetch that returns an HTML response detected by isLoginWallSignal (HTML content-type or '<!doctype'/'<html>'-style start tag with an HTTP status), passed through throwIfLoginWall before consumption — including results from inside page.evaluate.
Common situations: Session cookies expired so the site serves its login page at the API URL; hitting rate limits and receiving an HTML 429 challenge; Cloudflare/WAF interstitial pages; scraping endpoints while logged out or behind a captive portal.
Related errors
- Server returned HTML instead of JSON (status=${response.stat
- autohome ${contextHint} HTTP ${resp.status}
- Ctrip flight API returned HTTP ${status || 'unknown'}
- dongchedi ${contextHint} HTTP ${resp.status}
- eastmoney convertible failed: HTTP ${resp.status}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/bb44e6813fbc1b63.
Report an issue: GitHub.