The error, verbatim
Jump to the fix ↓Access to fetch at 'http://localhost:3001/plain/data' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Tested on
- Browser
- Chromium 152 (Chrome/152.0.7977.149)
- API
- Express 5.3.0, cors 2.8.6
- Front end
- Vite 8.3.4 dev server
- Node / OS
- 24.14.0 / Windows 11 Pro
Contents
Your page on one origin (here http://localhost:5173) called an API on another (http://localhost:3001). A different port is enough to count as a different origin. The browser sent the request, got a response, and then refused to let your JavaScript read it, because the response didn’t say this origin was allowed.
That’s why Postman and curl work: CORS is a browser rule, not a server one. I called the same URL with Node’s fetch and got {"ok":true}.
In your code, every version of this error looks identical: TypeError: Failed to fetch. The details are only in the browser console. I got five different versions in Chrome:
| What the server did | What the console said |
|---|---|
Sent no CORS headers, plain GET |
No 'Access-Control-Allow-Origin' header is present on the requested resource. |
Sent no CORS headers, POST with JSON |
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present... |
Set Access-Control-Allow-Origin: * by hand, POST with JSON |
Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response. |
Allowed *, request sent with credentials: 'include' |
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. |
| Rejected the request (401) before the CORS code ran | No 'Access-Control-Allow-Origin' header is present on the requested resource. |
The second and third rows involve a preflight: before a POST with a JSON body (or any custom header), the browser first sends an OPTIONS request asking for permission. If that answer is wrong, the real request is never sent.
One look-alike that isn’t CORS: with the API not running at all, fetch also threw Failed to fetch, but the console said net::ERR_CONNECTION_REFUSED and nothing about CORS.
Fix it on the API
If the API is yours, it has to send the headers. In Express, add the cors package before your routes:
npm i corsimport cors from 'cors';
app.use(cors());cors() with no options allows every origin. That’s fine for a public API. For anything else, name the site that may call it, as in the cookies section below.
The order of middleware matters
The last row of the table is the one that wastes the most time. My login check ran before cors() and answered 401. That response had no CORS headers, so the browser hid it, and the console reported a CORS problem instead of “not logged in”.
Put cors() first, before authentication, rate limits, or anything else that can answer early:
app.use(cors());
app.use(requireLogin);An error thrown inside a route was fine either way: with cors() first, the 500 response kept its headers and was readable.
Sending cookies: no wildcard
With credentials: 'include' in fetch, the browser refuses Access-Control-Allow-Origin: *. The server has to name your origin and allow credentials.
app.use(cors({ origin: 'http://localhost:5173', credentials: true }));Use your real front-end address in production.
Not your API: proxy it in development
If you can’t change the server, or just want local development to work, let the Vite dev server forward the requests. The browser then only talks to its own origin, so CORS never comes up.
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};Then call fetch('/api/plain/data') instead of the full URL.
server.proxy is a setting of the dev server. Your production build needs its own way to reach the API: CORS headers on the API, or the same proxy setup on whatever serves the site.
What didn’t work
How this was tested
Windows 11 Pro, Node 24.14.0, October 19, 2026. An Express 5.3.0 API on port 3001 served the same routes with no CORS headers, with a hand-set *, with cors 2.8.6, with credentials allowed, and with a 401 check before and after cors(). A page on the Vite 8.3.4 dev server (port 5173) called each one with fetch in Chromium 152 (Claude’s built-in browser), then again through the Vite proxy. Every console message above is the real output.
— N.K., end of entry No.080