Fix 'Unexpected token < in JSON'
The short version: you parsed something that is not JSON. That leading < is the first character of an HTML page (<!DOCTYPE html> or <html>) - the server returned an error page, a 404, or a login redirect, and your code called res.json() on it. Do not fix the parser; find out what the response actually was: log res.status and the raw res.text() before parsing, and fix why the server sent HTML.
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.