jackwener/OpenCLI · error · LoginWallError
Server returned HTML instead of JSON (status=${response.stat
Error message
Server returned HTML instead of JSON (status=${response.status}). Likely a login wall, rate limit, or WAF challenge. What it means
parseJsonOrThrowLoginWall checks the response content-type and the first characters of the body; if either indicates HTML, it throws LoginWallError (code LOGIN_WALL, exit 77) carrying the HTTP status, URL, and first 100 chars of the body. It exists to convert 'server gave me a webpage' into an actionable auth/rate-limit error rather than a JSON parse stack trace.
Source
Thrown at src/utils.ts:148
* 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. */
export async function parseJsonOrThrowLoginWall(
response: Response,
opts: { url?: string } = {},
): Promise<unknown> {
const contentType = response.headers.get('content-type') || '';
const text = await response.text();
const trimmed = text.trimStart();
const looksLikeHtml =
contentType.toLowerCase().includes('text/html')
|| /^<(?:!doctype|html|head|body|title)(?:[\s>/]|$)/i.test(trimmed);
if (looksLikeHtml) {
throw new LoginWallError(
`Server returned HTML instead of JSON (status=${response.status}). `
+ `Likely a login wall, rate limit, or WAF challenge.`,
response.status,
opts.url || response.url || '',
trimmed.slice(0, 100),
);
}
try {
return JSON.parse(text);
} catch (err) {
// Real malformed JSON \u2014 surface body preview alongside the parser message
// so we don't have to repro to know what came back.
throw new Error(
`JSON parse failed (status=${response.status}, body[0..50]=${JSON.stringify(trimmed.slice(0, 50))}): `
+ (err instanceof Error ? err.message : String(err)),
);
}View on GitHub (pinned to 49907e53dc)
Solutions
- Log in again in the browser session the CLI uses, then retry
- If status is 429 or the body mentions challenge/verify, back off and retry after a delay
- Verify the URL is the JSON API endpoint, not an HTML page that redirects
- Inspect err.bodyPreview to identify the wall type before automating retries
Example fix
// before
const data = await res.json();
// after
const data = await parseJsonOrThrowLoginWall(res, { url }); Defensive patterns
Strategy: try-catch
Validate before calling
const ct = res.headers.get('content-type') || '';
if (ct.includes('text/html')) throw new LoginWallError('HTML response', res.status, res.url, (await res.text()).slice(0, 100)); Type guard
function isLoginWallError(e: unknown): e is LoginWallError {
return e instanceof LoginWallError;
} Try / catch
try {
const data = await parseJsonOrThrowLoginWall(res, { url });
} catch (e) {
if (e instanceof LoginWallError) {
if (e.status === 429) await sleep(60_000);
else await relogin();
} else throw e;
} Prevention
- Use the typed error's status/bodyPreview to branch: 429 → back off, 401/403 → re-login
- Add exponential backoff for retries against rate-limiting endpoints
- Prefer API endpoints over page URLs that can redirect to HTML login pages
- Verify session validity with a cheap authenticated endpoint before long jobs
When it happens
Trigger: Calling parseJsonOrThrowLoginWall on a fetch whose response content-type includes 'text/html' or whose trimmed body starts with <!doctype/html/head/body/title — e.g. an API URL that redirected to a login page, or a 403/429 WAF page.
Common situations: Expired session cookies on sites like x.com/grok; scraping an endpoint that now requires auth; corporate proxies injecting HTML error pages; Cloudflare 'checking your browser' interstitials during bursts of requests.
Related errors
- Server returned HTML instead of JSON (status=${value.status}
- archive snapshots returned malformed JSON: ${error?.message
- autohome ${contextHint} HTTP ${resp.status}
- Ctrip flight API returned HTTP ${status || 'unknown'}
- dongchedi ${contextHint} HTTP ${resp.status}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/d264cd9188b688e0.
Report an issue: GitHub.