You write a fetch call, hit refresh, and the console fills with red. The request clearly reached the server, the data is right there in the network tab, and your code still cannot touch it. This error stops more frontend work than any other, and the fix is almost always one line.
Why the No Access-Control-Allow-Origin Header Error Happens
Your server did not tell the browser it was allowed to share the response.
Browsers block a page from reading responses fetched from a different origin unless the server explicitly permits it. Permission arrives as one response header:
Access-Control-Allow-Origin: https://your-app.com
No header, no permission. The request still reached the server and the server still replied — the browser simply refuses to hand the result to your JavaScript.
An origin is scheme plus host plus port. That makes http://localhost:3000 and http://localhost:5173 different origins, which surprises people on their own machine.
Check what your endpoint actually returns with our CORS tester before changing anything.
Confirm What the Server Is Really Sending
Guessing wastes more time than checking.
Open the network tab, click the failing request, and read the response headers. If Access-Control-Allow-Origin is absent, you have confirmed the diagnosis.
One caveat that catches everyone: browsers hide the underlying failure. If your route throws a 500, the error handler often skips the CORS middleware, so the response arrives bare and the browser reports CORS rather than the crash.
In our experience, roughly one CORS report in four is actually a 500 wearing a disguise. Read the server logs before touching the config.
Fixing Blocked by CORS Policy in Express
Install the cors package and configure it explicitly.
const cors = require('cors');
app.use(cors({
origin: ['https://your-app.com', 'http://localhost:3000'],
credentials: true,
}));
Two details matter. Register it before your routes, or requests are handled before the headers get added. And list origins explicitly rather than reflecting whatever arrives.
Reflecting the request origin back is a common shortcut that quietly allows every site on the internet. With credentials enabled that is a genuine security hole.
The other trap is error handling. Your CORS middleware runs on the happy path; make sure your error handler adds the header too, or every 500 looks like a CORS bug.
Adding the Access Control Allow Origin Header in Nginx
Three lines, plus one for the preflight.
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://your-app.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Vary' 'Origin' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
That always flag is the part people miss. Without it, Nginx skips the header on error responses, which recreates the exact problem described above.
Vary: Origin matters if anything caches. Without it a CDN can store the response for one origin and serve it to another, producing failures that appear at random.
Why the CORS Error Only Shows Up on localhost
Because your dev server is a different origin from your API.
In production the frontend and API often share a domain, so no cross-origin request happens and CORS never applies. Locally your app runs on one port and the API on another, which makes them different origins.
Two clean fixes. Add your localhost origin to the allowed list, which is what most teams do. Or proxy through the dev server so the browser only ever sees one origin:
// vite.config.js
server: {
proxy: { '/api': 'http://localhost:8080' }
}
We prefer the proxy for local work. It keeps development origins out of your production configuration, which is one fewer thing to forget to remove.
When You Do Not Control the API
You cannot fix it from the browser. Something server-side has to sit in between.
Three legitimate options:
- Your own backend proxy. Your server calls the API and returns the result. Server-to-server requests are not subject to CORS.
- A dev-server proxy. Fine locally, useless in production.
- Ask the provider. Many public APIs will add your origin if you ask.
What does not work: mode: 'no-cors'. It silences the error and returns an opaque response with status 0 and an empty body. You get quiet failure instead of a loud one.
Public CORS proxies are a fourth option we would think hard about. They see every request you send, including auth tokens, and they go down without warning.
Wrapping Up
The no Access-Control-Allow-Origin header error means the server never granted permission. Add the header, add it on error responses too, and name your origins explicitly.
Check the server logs before the config — a surprising share of these are crashes in disguise.
If the preflight itself is failing rather than the main request, see fixing a failing CORS preflight. For the background, what CORS is and why it exists explains what the browser is protecting.
