When an asynchronous request fails while parsing JSON, the problem is not necessarily in the JSON parser itself. The response may contain an HTML error page, an empty body, truncated data, an authentication redirect, or incorrectly decoded characters instead of the JSON the application expected.
These failures can originate in application code, backend services, reverse proxies, authentication layers, CDNs, or the client itself. This diagnostic guide explains how to inspect the actual HTTP response, identify common causes of invalid JSON, and handle unexpected response states safely in production — with every failure below reproduced against a real test server, not just described.
Analyzing network payloads and diagnosing invalid JSON responses
Tested: Three Endpoints, Three Outcomes
To make this concrete, we ran a small local API with three endpoints: one healthy JSON endpoint (/api/users), one that crashes and returns an HTML 500 page (/api/broken), and one that redirects an unauthenticated request to a login page (/api/login-redirect). Here is exactly what each one returned.
The broken endpoint: an HTML 500 page
curl -i against the crashing endpoint:
HTTP/1.0 500 Internal Server Error
Content-Type: text/html
Content-Length: 170
<!DOCTYPE html><html><head><title>500 Internal Server Error</title></head><body><h1>500 Internal Server Error</h1><p>Something went wrong on the server.</p></body></html>
There is no JSON here at all — just an HTML error page with a 500 status. Any code that calls response.json() on this will throw, and the error message will blame the JSON, not the server.
The redirect chain: what curl -i -L reveals
Without -L, curl stops at the redirect and shows you the truth:
HTTP/1.0 302 Found
Location: /api/users
With -L, curl follows the chain and shows every step — here the redirect lands on a healthy JSON endpoint:
HTTP/1.0 302 Found
Location: /api/users
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 34
{"users":[{"id":1,"name":"Alex"}]}
But the same redirect to a login page ends very differently — a 200 status with an HTML body, which is exactly the case that fools frontend code:
HTTP/1.0 302 Found
Location: /login
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 96
<!DOCTYPE html><html><head><title>Login</title></head><body><h1>Please log in</h1></body></html>
That final 200 OK is the trap. A status check alone passes, the content-type says text/html, and the body starts with <!DOCTYPE html>. If your code only checks response.ok, this one gets through — and then fails at parse time.
The fetchJson helper, run against all three
We ran the fetchJson helper from Section 6 against each endpoint. Against the healthy endpoint it works exactly as intended:
--- fetchJson(http://127.0.0.1:8766/api/users) ---
Status: 200 | redirected: false | final URL: http://127.0.0.1:8766/api/users
Content-Type: application/json
Response preview: {"users":[{"id":1,"name":"Alex"}]}
RESULT: {"users":[{"id":1,"name":"Alex"}]}
Against the crashing endpoint, the helper’s status check catches it before parsing, and the thrown error includes the raw HTML preview — so you see the server’s error page in the error message instead of a bare parse failure:
--- fetchJson(http://127.0.0.1:8766/api/broken) ---
Status: 500 | redirected: false | final URL: http://127.0.0.1:8766/api/broken
Content-Type: text/html
Response preview: <!DOCTYPE html><html><head><title>500 Internal Server Error</title></head><body><h1>500 Internal Server Error</h1><p>Something went wrong on the server.</p></body></html>
THREW: Request failed with status 500: <!DOCTYPE html><html><head><title>500 Internal Server Error</title></head><body><h1>500 Internal Server Error</h1><p>Something went wrong on the server.</p></body></html>
Against the login redirect, the helper still saves you — but through the parse-error path, not the status check, because the final status is 200. Notice redirected: true and the final URL pointing at /login:
--- fetchJson(http://127.0.0.1:8766/api/login-redirect) ---
Status: 200 | redirected: true | final URL: http://127.0.0.1:8766/login
Content-Type: text/html
Response preview: <!DOCTYPE html><html><head><title>Login</title></head><body><h1>Please log in</h1></body></html>
THREW: Server returned invalid JSON. Response preview: <!DOCTYPE html><html><head><title>Login</title></head><body><h1>Please log in</h1></body></html>
One improvement worth making to the helper: it never checks the Content-Type header. In the login-redirect run, a content-type check would have failed fast with a clear message (“expected application/json, got text/html”) instead of falling through to the parse error. The log line is already there — add the check right after it and this whole class of failure gets a readable error instead of a SyntaxError.
Quick Diagnostic Reference Table
If your application is throwing parsing errors on incoming API responses, use this reference table to isolate the failure point. The last column shows which of our test runs produced each symptom:
| API Parsing Symptom | Root Cause | Immediate Action | Seen in Our Test |
|---|---|---|---|
Unexpected token < at position 0 | Proxy, WAF, authentication redirect, or server crash returned an HTML error/login page | Inspect raw response body text | Yes — the 500 endpoint and the login redirect both returned HTML starting with <!DOCTYPE html>. See our deep dive into fixing an Unexpected Token in JSON for the full diagnosis of this exact error. |
| Unexpected end of JSON input | Client code attempted to parse a response with no JSON body, such as a 204 response or empty 200 response | Check response length and status code before parsing | Yes — parsing an empty string throws exactly this. Our guide to Unexpected End of JSON Input covers the truncated and empty cases in detail. |
| Garbled text or character corruption | Mismatched text encoding or intermediary translation corruption | Validate response headers and inspect raw bytes/text | Not reproduced in this run — see Section 5 for how to hunt it down. |
| 502 Bad Gateway / 504 Gateway Timeout | Upstream server failure injecting HTML proxy error screens | Check server health logs and reverse-proxy configurations | Same shape as our 500 run — an HTML page with a 5xx status and a text/html content-type. |
1. Content-Type Mismatch
A frequent backend failure occurs when an endpoint responds with a 200 OK status code, but populates the payload body with HTML or plain text instead of structured JSON. This usually happens when a routing fallback or maintenance script intercepts the request. Our login-redirect run is a live example: final status 200, content-type text/html, body an HTML login page.
Note that validating a Content-Type header alone does not guarantee the body contains valid syntax, as a server can return an application/json header alongside malformed text. If you want to dive deeper into what counts as valid in the first place, review our guide on JSON lint vs JSON validation — it shows, with real runs, the difference between a file that merely parses and data that actually matches its schema.
2. Empty Responses and the 204 Trap
Endpoints handling deletion or silent updates often return an empty body with a status of 204 No Content or an empty string with a 200 OK. A 204 status itself does not cause a parsing error; the issue occurs when client code blindly attempts to parse the empty response body as JSON. Specifically, calling await response.json() will fail when there is no JSON body, including a genuine 204 No Content response, resulting in an Unexpected end of JSON input syntax error.
We verified that exact error in our unexpected-token testing: parsing an empty string throws SyntaxError: Unexpected end of JSON input. The fetchJson helper above handles this with its empty-body check (if (!text.trim()) return null;) — check for an empty body before you parse, and a 204 stops being an error at all.
3. Authentication Redirects and Login Pages
An expired session, missing authentication token, incorrect API credentials, or middleware configuration can cause an API request to receive an HTML login page instead of JSON. For example, a request to GET /api/user might trigger a 302 Redirect to /login, returning an HTML login page. Because browser fetch() requests follow redirects automatically, developers often see a final 200 OK status code and assume the API worked, only to encounter an Unexpected token < error when parsing fails.
Our test run shows this playing out exactly as described: the redirect chain ended at /login with a 200 status, redirected: true, and an HTML body. The two things that expose it are response.redirected (true) and response.url (the login page, not your API endpoint). Log both in your fetch wrapper and this failure stops being mysterious.
4. Proxy, Gateway, and WAF Interceptions
When an application scales behind reverse proxies like Nginx, Cloudflare, or enterprise Web Application Firewalls (WAF), a backend crash or rate-limit trigger doesn’t reach your application code. Instead, the proxy intercepts the bad request and injects its own HTML template — such as a 502 Bad Gateway or a Cloudflare DDoS challenge screen.
Because these proxy screens start with standard HTML tags, client apps expecting JSON break immediately. Debugging these requires inspecting the network layer directly to see if the response originated from your application server or an upstream proxy. The tell is in the response headers: a proxy error page usually carries the proxy’s own Server header (e.g. cloudflare or nginx) rather than your application’s, and the status will be a 502, 503, or 504 rather than a 200. Our 500 test run shows the same pattern at a smaller scale — an HTML page with a 5xx status and a text/html content-type is never your JSON.
5. Encoding and Charset Problems
JSON exchanged over modern web APIs should be transmitted using UTF-8. Problems can occur when a legacy system generates bytes using a different character encoding, or when an intermediary incorrectly decodes and re-encodes the response before it reaches the client.
These problems do not always produce a JSON syntax error. Depending on where the encoding mismatch occurs, characters may appear corrupted, Unicode replacement characters may be introduced, or the resulting text may no longer match the JSON expected by the application.
For API responses, the server should identify the payload as JSON with an appropriate Content-Type header:
Content-Type: application/json; charset=utf-8
If characters are being corrupted, compare the raw response bytes, HTTP headers, server configuration, and any proxy or middleware that transforms the response. The goal is to determine where the original byte sequence was incorrectly encoded or decoded. One encoding trap we have verified elsewhere on this site is worth knowing about here: a UTF-8 byte order mark (\ufeff, charCode 65279) silently prepended to a payload breaks strict parsers at position 0 while looking like nothing at all — it’s covered in detail in our Unexpected Token in JSON guide, and it’s the first thing to check when a response looks perfect but still won’t parse.
6. Inspect the Raw Response Before Parsing
When debugging an invalid JSON response, the most useful first step is to inspect exactly what the server returned. Calling response.json() immediately attempts to parse the response body, so you may lose the opportunity to inspect the raw response text that caused the failure.
It is important to remember that fetch() does not reject its promise merely because the server returns a 4xx or 5xx HTTP error status. The application must explicitly inspect response.ok or response.status.
For troubleshooting, read the response as text first and inspect the status code, content type, and beginning of the body:
async function fetchJson(url) {
const response = await fetch(url);
const contentType = response.headers.get('content-type');
const text = await response.text();
console.log('Status:', response.status);
console.log('Content-Type:', contentType);
console.log('Response preview:', text.slice(0, 200));
// fetch does not reject on 4xx/5xx responses automatically
if (!response.ok) {
throw new Error(
`Request failed with status ${response.status}: ${text.slice(0, 200)}`
);
}
if (!text.trim()) {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(
`Server returned invalid JSON. Response preview: ${text.slice(0, 200)}`
);
}
}
We ran this exact helper against our three test endpoints (see the full logs at the top of this post). It returned the data cleanly for the healthy endpoint, threw a readable status error with the HTML preview for the 500 endpoint, and threw a parse error with the preview for the login redirect. This approach makes several common failures immediately visible. An HTML document usually begins with <, an authentication page may contain a login form, and a truncated response may end unexpectedly. If a payload does fail structurally, the next step is to repair the broken JSON — that guide walks through diagnosing and fixing malformed payloads once you’ve isolated the raw text.
7. Debug API Responses with curl
When browser developer tools are not enough, command-line diagnostics can reveal hidden redirect chains and header anomalies. You can inspect raw headers and status codes using:
curl -i https://example.com/api/data
The -i flag exposes the full HTTP response headers and status line. If you suspect an authentication redirect is silently converting your JSON response into an HTML login page, use the follow-redirects flag:
curl -i -L https://example.com/api/data
This command outputs every intermediate redirect step, allowing you to catch instances where an API route resolves to an HTML resource. Our test runs above show both sides of this: without -L, the redirect endpoint shows just 302 Found with a Location header; with -L, you see the full chain — the 302, then the final 200, and whether that final response is JSON (Content-Type: application/json) or an HTML login page (Content-Type: text/html). When the chain ends on HTML, you’ve found your bug without opening a browser.
The Definitive API Troubleshooting Checklist
When an API endpoint starts throwing invalid JSON errors across your application stack, systematically walk through this triage list:
- Check the Network Tab: Do not rely solely on console error logs. Open your browser DevTools, click the failing network request, and look at the raw Response tab.
- Verify the Leading Character: If the response starts with
<, an HTML wrapper, proxy error, or login redirect has replaced your JSON data. - Inspect Content-Type Headers: Confirm whether the server is explicitly declaring
application/json, keeping in mind that content type headers alone do not guarantee syntactic validity. - Validate Data Integrity: Paste the raw failing response payload into our free online JSON formatter and validator to see if the server output is truncated or structurally corrupted.
- Review Server & Proxy Logs: Check upstream error logs (Nginx error logs, Node/Python process logs) to see if an unhandled exception triggered an HTML fallback screen.
How to Prevent Invalid JSON Responses in Production
Preventing parsing crashes requires hardening both ends of the data pipeline. On the server side, ensure routes consistently return proper headers, handle application exceptions gracefully without dumping HTML traces, and explicitly return JSON error payloads rather than triggering silent redirects for API clients. On the client side, never assume an endpoint is infallible; structure your JavaScript network wrappers to inspect headers, handle empty 204 states cleanly, and safely catch parsing anomalies before they break application state.
The single highest-value change on the client side is the one demonstrated at the top of this post: read every response as text first, log the status, content-type, and a 200-character preview, and only then parse. That one habit turns every failure in this guide — the HTML 500, the silent login redirect, the empty 204, the truncated body — from a cryptic SyntaxError into a log line that names the actual cause.
Frequently Asked Questions
Why does my API return HTML instead of JSON?
Because something between your code and the API returned an HTML page instead of your data: a server crash (500), a missing route (404), an expired session redirecting to a login page, or a proxy/WAF injecting an error screen. We reproduced the first two live — a crashing endpoint returned a 500 HTML page, and a redirect ended on an HTML login page with a 200 status. Check the status code, content-type, and first characters of the raw body to see which one it is.
Should I use response.json() or response.text()?
Use response.text() first when debugging, and in any fetch wrapper that needs to survive bad responses. response.json() throws a bare SyntaxError and consumes the body, so you lose the chance to see what the server actually sent. Read as text, log the status, content-type, and a preview, check for an empty body, then parse — that’s the fetchJson helper in Section 6, tested against all three of our endpoints.
How do I catch a redirect that turns my JSON into a login page?
Check response.redirected and response.url after the fetch. In our test run, the login redirect showed redirected: true with the final URL at /login — even though the status was a clean 200. curl -i -L shows the same thing from the command line: the 302, then the final 200 with Content-Type: text/html.
What does “Unexpected end of JSON input” mean from an API?
It means you tried to parse an empty body — a 204 No Content response, or a 200 with an empty string. Parsing an empty string throws exactly that error (verified in our testing). Guard against it by checking if (!text.trim()) return null; before parsing, as the fetchJson helper does. The truncated-body version of this error is covered in our Unexpected End of JSON Input guide.
Can a 502 or 504 error cause a JSON parse failure?
Yes. A 502 Bad Gateway or 504 Gateway Timeout from a reverse proxy usually comes with an HTML error page, not JSON — same shape as the 500 page in our test run, just from the proxy instead of your application server. Check the Server response header to tell whether the HTML came from your app or from an upstream proxy, then check the proxy and upstream health logs.
Summary
APIs return invalid JSON for a small set of repeatable reasons: an HTML error page from a crash or a proxy, a silent redirect to a login page, an empty 204 body, or an encoding problem. We reproduced the first three against a real test server — a 500 HTML page, a redirect chain ending on an HTML login page with a 200 status, and a healthy endpoint for contrast — and ran the fetchJson helper against all three to show exactly which check catches each failure. The fix is a habit, not a patch: read every response as text first, log the status, content-type, and a short preview, handle empty bodies before parsing, and only then call JSON.parse. Do that, and an “invalid JSON” error stops being a mystery and starts naming its cause.
