"Blocked by CORS policy": what the browser is really refusing, and how to fix it on the server
Fix "has been blocked by CORS policy: No Access-Control-Allow-Origin header": why it happens, the fix on the server or in Nginx, preflight requests, and cookies.
by OhSudo ··
Not sure this is your cause?Find it step by step: Website down →
Part of a learning path:Web servers: nginx and HTTPS in production
Access to fetch at 'https://api.example.com/items' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.The first surprise: your request probably worked. The server answered, and the browser is the one refusing to hand that answer to your JavaScript, because the server didn’t say this other site may read it. That’s why it works in curl, Postman or a new tab, as those don’t apply CORS.
The second surprise: you can’t fix this in your front-end code. The permission has to come from the server that’s being called.
- Your pagehttps://app.example.comfetch()
- The APIanswers, without Access-Control-Allow-Originno permission header(fails)
- The browserblocks the answer from your JavaScript(breaks here)
TL;DR
Make the API answer with the header, for your site only:
Access-Control-Allow-Origin: https://app.example.comVary: OriginOr avoid CORS altogether, by serving the API and the app from one address, with a proxy. To see what your server really sends:
curl -sI -H 'Origin: https://app.example.com' https://api.example.com/items1. What’s an origin?
An origin is the scheme, name and port together. All of these are different origins, so each of these calls needs CORS:
https://app.example.com → https://api.example.com another namehttps://example.com → http://example.com another schemehttp://localhost:3000 → http://localhost:8000 another portThe last one is why you meet this in development so often.
2. See what the server sends
Pretend to be your site, and read the headers:
curl -sI -H 'Origin: https://app.example.com' https://api.example.com/itemsPaste that into the HTTP Headers Analyzer, which points out the usual CORS mistakes. Your browser’s Network tab shows the same thing. Is there a response at all? A 500 from your code often has no CORS headers, since an error stopped it before they were added. The browser then reports a CORS error, which hides the real one. Look at the status code first.
3. Fix it in the application
Using your framework’s support is usually best.
Express:
import cors from 'cors';app.use(cors({ origin: 'https://app.example.com' }));FastAPI:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware( CORSMiddleware, allow_origins=["https://app.example.com"], allow_methods=["*"], allow_headers=["*"],)4. Or fix it in Nginx
map $http_origin $cors_origin { default ""; "https://app.example.com" $http_origin; "https://admin.example.com" $http_origin;}
server { listen 8080;
location /api/ { add_header Access-Control-Allow-Origin $cors_origin always; add_header Vary Origin always;
if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin 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 3600 always; add_header Vary Origin always; return 204; }
proxy_pass http://127.0.0.1:3000; }}The map allows a list of sites, and answers with just the one that’s asking, since the header can only hold one origin. always keeps it on error responses, where it’s easy to lose.
Do it in one place. If your application and Nginx both add it, the browser receives two, and refuses:
The 'Access-Control-Allow-Origin' header contains multiple values 'https://app.example.com, https://app.example.com', but only one is allowed.5. The simplest fix: no CORS at all
If the app and its API sit behind the same address, then there’s nothing to allow. With a proxy, /api is on the same origin:
location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host;}The guide for React and Vite has the full setup. In development, Vite’s server.proxy setting does the same, so your code can call /api.
6. Preflight requests
Before a “non-simple” request, the browser asks permission, with an OPTIONS request. That covers PUT, DELETE, JSON bodies, and headers like Authorization:
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.So your server has to answer OPTIONS with a 2xx, and these headers. Test it:
curl -si -X OPTIONS https://api.example.com/items \ -H 'Origin: https://app.example.com' \ -H 'Access-Control-Request-Method: PUT' \ -H 'Access-Control-Request-Headers: authorization'A login check that rejects OPTIONS is a typical cause, since a preflight carries no credentials. So the 401 or 403 gets reported as a CORS failure.
7. Cookies and credentials
With credentials: 'include', the rules get stricter:
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'.So you need a specific origin, plus this:
Access-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Credentials: trueThe cookie itself must be SameSite=None; Secure for it to travel between sites. So this only works over HTTPS.
Warning
Don’t reflect whatever Origin arrives while allowing credentials, as some snippets do. It lets any website make logged-in requests as your users, and read the answers. Allow specific sites. Browser extensions that “disable CORS” only hide the problem on your own machine, and your users won’t have them. * is fine for truly public data with no login.
Shell, we have a problem!
Similar, but different: Mixed Content means an HTTPS page calling plain HTTP. net::ERR_CERT_DATE_INVALID is an expired certificate. ERR_TOO_MANY_REDIRECTS is a redirect loop, and a redirect during preflight fails too. A 502 or 504 from Nginx often shows up as a CORS error as well, so check Nginx 502. The Nginx Config Generator writes the proxy setup.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.