Common causes
1. The URL is wrong and the server sent a 404 page
A typo, a missing version prefix or a path that only exists in another environment returns the server’s HTML 404 page. fetch does not reject on HTTP errors, so check res.ok before parsing.
const res = await fetch('/api/v2/user');
const users = await res.json();const res = await fetch('/api/v2/users');
if (!res.ok) throw new Error(`GET /api/v2/users failed: ${res.status}`);
const users = await res.json();2. A relative URL resolved against the current page
Without a leading slash, api/users is resolved relative to the page, so on /dashboard/ the request goes to /dashboard/api/users. Single-page apps answer unknown paths with index.html, which starts with <!DOCTYPE html>.
const res = await fetch('api/users');const res = await fetch('/api/users');3. The dev server has no API proxy
In development, Vite, webpack-dev-server and Create React App serve index.html for any path they do not know. Without a proxy rule, /api calls never reach your backend.
export default {
server: { port: 5173 },
};export default {
server: {
port: 5173,
proxy: { '/api': 'http://localhost:8080' },
},
};4. An expired session redirected to a login page
Many apps answer an unauthenticated API call with a 302 redirect to the login form, and fetch follows it silently. Check the content type so the real problem (the session) is reported instead of a parse error.
const data = await res.json();const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
throw new Error(`Expected JSON but got ${type} from ${res.url}`);
}
const data = await res.json();Frequently asked questions
How do I see the HTML my code received?
Open the browser developer tools, go to the Network tab, click the request and open the Response panel. In code, call res.text() instead of res.json() and log the first few hundred characters.
Why does the API work in the browser address bar but not in my app?
The address bar sends your cookies and follows redirects visibly, and its URL is absolute. Your app may be calling a relative URL, missing an auth header, or hitting a different origin that returns an error page.
Does this error mean my server is down?
Not necessarily, but it can. Proxies and load balancers such as nginx or Cloudflare return HTML pages for 502 and 503 errors, so a failing backend also produces a < at position 0.