{"record":{"id":"bb44e6813fbc1b63","repo":"jackwener/OpenCLI","slug":"server-returned-html-instead-of-json-status-val","errorCode":null,"errorMessage":"Server returned HTML instead of JSON (status=${value.status}). Likely a login wall, rate limit, or WAF challenge.","messagePattern":"Server returned HTML instead of JSON \\(status=(.+?)\\)\\. Likely a login wall, rate limit, or WAF challenge\\.","errorType":"exception","errorClass":"LoginWallError","httpStatus":null,"severity":"error","filePath":"src/utils.ts","lineNumber":116,"sourceCode":"}\n\nfunction isLoginWallSignal(v: unknown): v is LoginWallSignal {\n  return (\n    typeof v === 'object'\n    && v !== null\n    && (v as Record<string, unknown>).__loginWall === true\n    && typeof (v as Record<string, unknown>).status === 'number'\n  );\n}\n\n/** Throw a `LoginWallError` if `value` is the sentinel returned by the\n * browser-side sniffer; otherwise return `value` unchanged. Adapters that\n * fetch from inside `page.evaluate` call this on the result before consuming\n * it, so the Node-side gets a typed error instead of a JSON-parse stack\n * trace. */\nexport function throwIfLoginWall<T>(value: T, opts: { url?: string } = {}): T {\n  if (isLoginWallSignal(value)) {\n    throw new LoginWallError(\n      `Server returned HTML instead of JSON (status=${value.status}). `\n      + `Likely a login wall, rate limit, or WAF challenge.`,\n      value.status,\n      opts.url || value.url || '',\n      value.bodyPreview,\n    );\n  }\n  return value;\n}\n\n/** Parse a `Response` body as JSON, throwing `LoginWallError` if the server\n * returned an HTML page (login wall / rate limit / WAF interception) instead\n * of the expected JSON. Catches the common case of `<!DOCTYPE` or `<html`\n * leading the body \\u2014 naive `JSON.parse` on these gives a cryptic\n * `SyntaxError` that callers can't distinguish from \"real\" malformed JSON.\n *\n * On real (non-HTML) JSON-parse failures, throws a regular `Error` with a\n * body preview attached so debugging doesn't require a packet capture. */","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/utils.ts#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst data = JSON.parse(await page.evaluate(fetchJson, url));\n// after\nconst raw = await page.evaluate(fetchJson, url);\nconst data = throwIfLoginWall(JSON.parse(raw), { url });","handlingStrategy":"try-catch","validationCode":"// Pre-flight: probe the endpoint once and check content-type before batch work\nconst probe = await fetch(url);\nif ((probe.headers.get('content-type') || '').includes('text/html')) throw new Error('login wall detected before start');","typeGuard":"function isLoginWallError(e: unknown): e is LoginWallError {\n  return e instanceof LoginWallError;\n}","tryCatchPattern":"try {\n  const data = throwIfLoginWall(raw, { url });\n} catch (e) {\n  if (e instanceof LoginWallError) {\n    console.error(`Walled at ${e.url} (status ${e.status}): ${e.bodyPreview}. Re-login or back off.`);\n  } else throw e;\n}","preventionTips":["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"],"tags":["http","login-wall","rate-limit","waf","network"],"backgroundTag":"html-instead-of-json","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}