See what actually came back
Stop parsing blindly. Log the status and the raw body first - the answer is almost always right there:
const res = await fetch(url);
if (!res.ok || !res.headers.get("content-type")?.includes("application/json")) {
const body = await res.text();
throw new Error(`Expected JSON, got ${res.status}: ${body.slice(0, 120)}`);
}
const data = await res.json();
If that body starts with <!DOCTYPE html>, you have your answer: the server sent a web page, not data.
Why HTML came back
The endpoint errored
A 500 or unhandled exception rendered the framework HTML error page instead of a JSON body.
Wrong URL
You hit the frontend or a catch-all 404 page - a missing /api prefix, a bad base URL, or a typo in the path.
Auth redirect
An unauthenticated request was redirected to an HTML login page instead of returning 401 JSON.
Proxy / gateway page
A reverse proxy returned its own HTML 502/504 page because the app behind it was down or slow.
The parser is the messenger, not the bug
It is tempting to wrap JSON.parse in a try/catch and move on - but that swallows the real signal. An HTML body where you expected JSON is the upstream telling you something failed: it errored, you asked the wrong thing, or auth rejected you. Surface the status and the first part of the body, and the cryptic JSON exception becomes an obvious “the API returned a 502 page.”
If the body is a proxy error page, the real fix is upstream - see the 502 and 504 guides. Paste a snippet into the JSON formatter to confirm what you actually received.
See the real response, not the parser’s complaint
This error is a layer away from its cause - the parser fails, but the problem is what the upstream sent. On your own servers, Infraveil traces the actual request and response between your services - the status, the content-type, what the upstream really returned - so “Unexpected token <” becomes “the orders API has been returning its 500 page since 14:02,” recorded and inspectable.
Frequently asked questions
What does “position 0” mean?
The very first character is already invalid - the parser failed immediately. JSON never starts with <, so a < at position 0 is the start of an HTML document. A different position usually means truncated or malformed JSON instead.
Why did the server send HTML instead of JSON?
Usually: the endpoint errored and rendered an HTML error page (500/404), you hit the wrong URL (frontend or catch-all 404), an auth layer redirected to a login page, or a proxy returned its own 502/504 HTML. The status code and raw body tell you which.
How do I handle this safely in code?
Check res.ok and that content-type is application/json before parsing. If not JSON, read res.text() and surface it - an unexpected HTML body is information. Parsing blindly turns a clear upstream error into a cryptic exception layers away.
It works locally but fails in production - why?
The URL or upstream differs: a relative path now hitting a 404 page, an env var pointing at the wrong host, or a proxy returning HTML where dev returned JSON. Log the full URL and raw response in the failing environment - it is rarely the parser.
Client-side, no signup — they run in your browser.