Learn the theory
CORS & Preflight Simulator: how it works
5 min read · Updated
CORS (Cross-Origin Resource Sharing) is the mechanism that lets a server tell the browser: “yes, JavaScript from that origin may read my responses.” It exists because browsers enforce the same-origin policy — and it is the source of the most-searched error in front-end development. Once you know the three questions the browser asks (is it cross-origin? does it need a preflight? do the response headers opt in?), every CORS error becomes diagnosable in seconds.
In short
- An origin is scheme + host + port.
https://app.example.comandhttps://api.example.comare different origins. - CORS is enforced by the browser, not the server.
curland server-to-server calls ignore it. - “Non-simple” requests (JSON bodies,
Authorization,PUT/DELETE…) trigger a preflightOPTIONSrequest first. - The server opts in with
Access-Control-Allow-*response headers. No header, no access to the response. - A blocked request can still reach and change the server — CORS hides the response, it doesn't stop the request.
Same-origin policy and what an origin is
Without the same-origin policy, any website you open could use your logged-in session to read your email or bank balance by quietly calling those sites from JavaScript. So browsers refuse to let a page read responses from a different origin unless that origin agrees.
| URL | Same origin as https://app.example.com? | Why |
|---|---|---|
https://app.example.com/page2 | Yes | Only the path differs |
http://app.example.com | No | Different scheme (http vs https) |
https://api.example.com | No | Different host |
https://app.example.com:8443 | No | Different port |
What CORS is — and what it isn't
CORS is a controlled relaxation of the same-origin policy. The browser attaches an Origin header to cross-origin requests; the server answers with Access-Control-Allow-Origin (and friends) to say who may read the result.
It also doesn't apply to everything: <img>, <script src> and form posts have always been allowed cross-origin. CORS governs fetch(), XMLHttpRequest, web fonts and a few other APIs that expose the response to JavaScript.
Simple requests vs preflighted requests
A request is “simple” — sent immediately with no permission check — only if all of these are true:
- The method is
GET,HEADorPOST. - The only headers are CORS-safelisted ones (
Accept,Accept-Language,Content-Language,Content-Type, …). - If
Content-Typeis set, it istext/plain,application/x-www-form-urlencodedormultipart/form-data.
Anything else — PUT, DELETE, PATCH, Content-Type: application/json, an Authorization header or a custom header — needs a preflight: an automatic OPTIONS request asking the server for permission before the real request is sent.
OPTIONS /v1/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 86400Access-Control-Max-Age tells the browser how long it may cache this answer, so repeat calls skip the extra round trip (browsers cap it: roughly 2 hours in Chrome, 24 hours in Firefox).
The headers that matter
| Header | Direction | Purpose |
|---|---|---|
Origin | Request | Who is asking. Added by the browser; scripts can't change it. |
Access-Control-Request-Method / -Headers | Preflight request | Describes the real request that will follow. |
Access-Control-Allow-Origin | Response | The origin allowed to read the response — an exact origin or *. |
Access-Control-Allow-Methods | Preflight response | Methods the server accepts cross-origin. |
Access-Control-Allow-Headers | Preflight response | Request headers the server accepts. |
Access-Control-Allow-Credentials | Response | true lets the page send and receive cookies/HTTP auth. |
Access-Control-Max-Age | Preflight response | How long the preflight result may be cached. |
Vary: Origin | Response | Tells caches the response differs per Origin — required when you echo the origin. |
Cookies and credentials
By default cross-origin fetch() sends no cookies. Opting in with credentials: 'include' triggers stricter rules, and each one is a common source of errors:
Access-Control-Allow-Origin*cannot be `**. It must echo the exact origin- The response must include
Access-Control-Allow-Credentials: true(on the preflight response too). *inAllow-HeadersorAllow-Methodsis treated as a literal string, not a wildcard.- The wildcard in
Allow-Headersnever coversAuthorization— list it explicitly.
Common errors and how to fix them
| Console message | Cause | Fix |
|---|---|---|
| No 'Access-Control-Allow-Origin' header is present | The response has no CORS header (or the OPTIONS handler returned an error page). | Send Access-Control-Allow-Origin on every response, including errors and OPTIONS. |
| …must not be the wildcard '*' when the request's credentials mode is 'include' | Cookies requested but the server answered *. | Echo the exact origin and add Vary: Origin. |
| Request header field authorization is not allowed by Access-Control-Allow-Headers | The preflight response didn't list the header. | Add it to Access-Control-Allow-Headers. |
| Method PUT is not allowed by Access-Control-Allow-Methods | Method missing from the preflight response. | Add it to Access-Control-Allow-Methods. |
| Response to preflight request doesn't pass access control check: It does not have HTTP ok status | The API returned 404/401/405 for OPTIONS. | Handle OPTIONS before auth and routing, return 204. |
Configuring a server
Most frameworks ship CORS middleware. Register it before authentication so preflights aren't rejected:
// Express
import cors from 'cors';
app.use(cors({
origin: ['https://app.example.com'], // allow-list, echoed back
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['content-type', 'authorization'],
credentials: true,
maxAge: 86400,
}));# Nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "content-type, authorization";
add_header Access-Control-Max-Age 86400;
return 204;
}
}Frequently asked questions
- Why do I get a CORS error in the browser but not in Postman or curl?
- CORS is enforced by browsers. Postman and curl don't run the same-origin policy, so they send the request and show you the response regardless of CORS headers.
- Does a CORS error mean the request wasn't sent?
- Not necessarily. For simple requests the server receives and processes the request; the browser only withholds the response from your JavaScript. Only a failed preflight stops the real request from being sent.
- What is a preflight request?
- An automatic
OPTIONSrequest the browser sends before a non-simple cross-origin request, asking the server which origin, method and headers it accepts. - Can I use Access-Control-Allow-Origin: * with cookies?
- No. When credentials are included the browser rejects the wildcard. Respond with the exact origin and set
Access-Control-Allow-Credentials: true. - How do I avoid CORS entirely in development?
- Serve the API from the same origin through a dev-server proxy (Vite, webpack, Next.js rewrites). The browser then sees same-origin requests. Don't disable web security in the browser.
- What does mode: 'no-cors' do?
- It lets you send a simple cross-origin request but gives you an opaque response you cannot read. It's useful for fire-and-forget calls such as analytics beacons, not for reading data.