Short answer
Open the Network tab, click the failing request and look at the response: it will be an HTML page. If it is your site’s index.html, the URL is wrong or a single-page app rewrite catches /api routes before your API does. If it is a 404, 500 or login page, fix that first. In code, check res.ok and the content-type before calling res.json(), and log await res.text() when it fails so you can see what came back.
Why the response is HTML, and the fix
01
The request got an error page
“A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.).” MDN, Window: fetch() method
fetch() resolves for 404 and 500 responses too, so the code goes on to parse the server’s HTML error page as JSON. Check res.ok before reading the body, and show the status when it fails.
const res = await fetch('/api/orders'); if (!res.ok) { throw new Error(`API ${res.status}: ${(await res.text()).slice(0, 200)}`); } const data = await res.json();02
The URL points at the front end, not the API
A relative URL like /api/users goes to the site that served the page. In development that is often the Vite or React dev server, which answers unknown paths with index.html. Use the full API URL from an environment variable, or set up the dev server proxy. In production, check the deployed API base URL is set.
// vite.config.js: send /api to the backend in development export default { server: { proxy: { '/api': 'http://localhost:4000' } }, };03
A hosting rewrite catches the API route
Single-page apps on Netlify, Vercel or Firebase Hosting rewrite every path to /index.html so client routing works. If a request to /api/... matches that catch-all instead of a real API route, you get the HTML page with status 200. On Netlify and Firebase Hosting, rules run in order, so put the API or function rule before the catch-all. On Vercel, a file in the api folder wins over a rewrite, so the HTML means the route does not exist: check the file name, the path and that the function deployed.
04
A login redirect or a file that is not JSON
“This string has to be valid JSON and will throw this error if incorrect syntax was encountered.” MDN, SyntaxError: JSON.parse: bad parsing
A request without a valid session can be redirected to a login page, and fetch follows the redirect and gets HTML. Check that the auth header or cookie is sent. When you parse a file or string yourself, make sure it really is JSON: no trailing commas, double quotes around names and strings.
Checked against the vendor’s docs on : MDN: Window: fetch() method (HTTP errors do not reject); MDN: Response: ok property; MDN: SyntaxError: JSON.parse: bad parsing.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
Related: Page not found on refresh (Netlify, Vercel) · “Blocked by CORS policy” · “Cannot read properties of undefined”