CORS Errors Explained: How to Fix "No 'Access-Control-Allow-Origin' Header"
You call your API from the browser and the console fills with red: blocked by CORS policy. The confusing part is that the request usually did reach your server — often it even succeeded. CORS doesn't stop the request; it stops your JavaScript from reading the response. That's also why the same call works fine in curl or Postman: CORS is enforced by browsers, and only browsers.
What counts as cross-origin
An origin is the scheme, host and port together. These are all different origins:
https://app.example.comandhttps://api.example.com— different hosthttp://localhost:3000andhttp://localhost:8080— different porthttp://example.comandhttps://example.com— different scheme
When a page on one origin fetches from another, the browser asks the server for permission through response headers, chiefly Access-Control-Allow-Origin.
Simple requests and preflights
Some requests are sent straight away, and the browser checks the response headers afterwards. Others trigger a preflight: an automatic OPTIONS request that asks whether the real request is allowed. A preflight happens when, among other things, the request:
- uses a method other than GET, HEAD or POST,
- sends a header outside a short safe list —
Authorizationis the classic one, or - sends
Content-Type: application/json(onlytext/plain,multipart/form-dataandapplication/x-www-form-urlencodedare exempt).
So almost every real API call — JSON body, bearer token — is preflighted. The server must answer that OPTIONS request successfully, with the right headers, before the browser sends the real one.
The common errors, and their fixes
"No 'Access-Control-Allow-Origin' header is present on the requested resource." The response didn't include the header. Add it, with the calling origin as the value:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Vary: Origin matters when you choose the value per request: it stops a CDN from serving one origin's response to another.
"Response to preflight request doesn't pass access control check: It does not have HTTP ok status."
Your server answered the OPTIONS preflight with an error — often 401 because auth middleware ran first, or 404/405 because no route handles OPTIONS. Answer preflights before authentication, with 204 No Content and the CORS headers.
"Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response." List every custom header the client sends:
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
"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'."
Requests that send cookies (credentials: "include") can't use *. Return the specific origin plus Access-Control-Allow-Credentials: true.
"Redirect is not allowed for a preflight request." The preflight hit a redirect — commonly a trailing-slash or http→https rule. Call the final URL directly.
Server examples
Express, with the cors package and an explicit allow-list:
import cors from "cors";
app.use(cors({
origin: ["https://app.example.com", "http://localhost:5173"],
credentials: true,
maxAge: 600, // cache the preflight for 10 minutes
}));
A Next.js route handler answers preflights by exporting OPTIONS:
const CORS = {
"Access-Control-Allow-Origin": "https://app.example.com",
"Access-Control-Allow-Methods": "GET, POST",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
};
export function OPTIONS() {
return new Response(null, { status: 204, headers: CORS });
}
export async function POST(request: Request) {
return Response.json({ ok: true }, { headers: CORS });
}
nginx in front of an API:
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 600 always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
proxy_pass http://127.0.0.1:3000;
}
The always flag makes nginx add the header to error responses too — without it, a 500 from your API shows up in the browser as a CORS error, which hides the real problem.
What not to do
- Don't reflect any origin with credentials. Echoing whatever
Originarrives, together withAccess-Control-Allow-Credentials: true, lets any website read your users' data with their cookies. Check origins against an allow-list. - Don't use
mode: "no-cors". It makes the error disappear by giving you an opaque response you can't read. The request "works"; your code gets nothing. - Don't fight CORS in development. Use your dev server's proxy (Vite's
server.proxy, Next.jsrewrites) so the browser sees one origin.
Testing a preflight with curl
curl doesn't enforce CORS, but it can show you exactly what the browser would see:
curl -i -X OPTIONS https://api.example.com/items \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization, content-type"
Look for a 2xx status and the three Access-Control-Allow-* headers. To check what a list of endpoints returns, paste them into the HTTP Status Checker.
One last tip: a preflight is cached for as long as Access-Control-Max-Age says — up to 2 hours in Chrome and 24 hours in Firefox — so after changing CORS headers, test in a fresh private window.
For each header in detail, see the reference pages for Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials and Origin.