“Access to fetch at ‘https://api.example.com’ from origin ‘https://app.example.com’ has been blocked by CORS policy.” Almost every web developer meets this error. It makes sense once you understand what the browser is protecting.
The same-origin policy
An origin is the combination of scheme + host + port:
| URL | Same origin as https://app.example.com? |
|---|---|
https://app.example.com/page |
Yes |
http://app.example.com |
No (scheme) |
https://api.example.com |
No (host) |
https://app.example.com:8443 |
No (port) |
Browsers let a page send requests anywhere, but by default JavaScript can’t read responses from other origins. Without this, any website you visit could use your logged-in cookies to read your email or bank pages through your browser.
CORS (Cross-Origin Resource Sharing) is how a server says: “I’m fine with JavaScript from these origins reading my responses.”
Simple requests
Some requests are sent directly: GET, HEAD or POST with “simple” headers and content types (application/x-www-form-urlencoded, multipart/form-data, text/plain). The browser adds an Origin header and checks the response:
Request: Origin: https://app.example.com
Response: Access-Control-Allow-Origin: https://app.example.com
If the response’s Access-Control-Allow-Origin doesn’t match, the browser blocks JavaScript from reading it. (The request still reached the server — CORS isn’t a server-side access control.)
Preflighted requests
Anything else — PUT, PATCH, DELETE, Content-Type: application/json, custom headers like Authorization — triggers a preflight: an OPTIONS request asking permission first.
OPTIONS /api/items
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
The server must answer with:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Only then does the browser send the real request. Max-Age lets the browser cache the permission.
Credentials (cookies)
By default, cross-origin fetch doesn’t send cookies. With credentials: 'include', the server must respond with:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com (an exact origin — not *)
The wildcard * is not allowed together with credentials. Cookies also need suitable SameSite attributes (SameSite=None; Secure for cross-site cookies).
Reading response headers
JavaScript can only read a few “safe” response headers by default. Expose others explicitly:
Access-Control-Expose-Headers: X-Total-Count, Content-Disposition
Common errors and what they mean
| Browser message | Fix |
|---|---|
No Access-Control-Allow-Origin header |
Server doesn’t send CORS headers for this origin (or an error page lacks them) |
…header contains multiple values |
CORS set twice (app and proxy) — choose one |
…must not be the wildcard '*' when credentials mode is 'include' |
Return the specific origin |
Response to preflight request doesn't pass access control check |
OPTIONS not handled, or returns a redirect/404/401 |
Request header field authorization is not allowed |
Add it to Access-Control-Allow-Headers |
Tip: error responses (401, 500) also need CORS headers, or the browser hides the real error behind a CORS message.
Secure configuration
- List allowed origins explicitly; don’t reflect any
Originback blindly — that effectively allows every site, including with credentials. - Use
*only for truly public, cookie-less resources (public APIs, fonts, open data). - Add
Vary: Originwhen the response depends on the origin, so caches don’t mix them up. - Remember: CORS doesn’t stop curl, scripts or other servers. Protect APIs with authentication and rate limiting.
Or avoid CORS entirely
Serve the front end and API from the same origin — e.g. https://example.com and https://example.com/api/ via a reverse proxy. No cross-origin requests, no CORS. See deploying a React app with Nginx.
Configuration guides: CORS in Nginx. Related: API security best practices.