Origins, CORS, and the preflight
Almost everything a browser enforces about who may read what is expressed in one unit: the origin. That is the scheme, the host and the port, taken together, and all three must match.
https://example.com/a and https://example.com/b same originhttps://example.com and http://example.com different (scheme)https://example.com and https://api.example.com different (host)https://example.com and https://example.com:8443 different (port)The same-origin policy says a page may not read a response from another origin. It has never said a page may not send one — and that distinction is where nearly all the confusion lives.
The request is sent. The answer is withheld.
In that first exchange the cross-origin GET went out, reached the server, was executed, and came back. The browser then declined to hand the response to the page, because the response carried no header saying it was allowed to.
CORS is therefore a relaxation mechanism, not an enforcement one. It is how a server opts in to being read by another origin. It protects the server not at all — anything that is not a browser ignores it entirely — and it is why "CORS blocked my request" is almost always the wrong description of what happened.
When permission is asked first
Some requests are too dangerous to send speculatively, so the browser asks first with an
OPTIONS request — a preflight — and only sends the real one if the answer allows it.
The dividing line is historical rather than principled: a request is "simple" if a 1999 HTML form could have made it. That means GET, HEAD, and POST with form-ish content types, and a short list of headers.
| Request | Simple? | Because |
|---|---|---|
GET with no custom headers | Yes | A link could do it |
POST of application/x-www-form-urlencoded | Yes | A form could do it |
POST of application/json | No | No form could produce that content type |
Anything with Authorization | No | Not a header a form could set |
PUT, PATCH, DELETE | No | Forms cannot issue these methods |
So the modern JSON API call is never simple, and pays a full extra round trip before its
first request. Access-Control-Max-Age caches that permission, and almost nobody sets
it.
CORS is defined in the WHATWG Fetch Standard rather than in an RFC, which is worth knowing when you go looking for it: there is no RFC number to cite, and the living standard is the authority.
Key takeaways
- An origin is scheme, host and port together. All three must match, so https and http are different origins.
- The same-origin policy governs reading a response, never sending a request.
- CORS is how a server opts in to being read by another origin. It relaxes a browser restriction and protects the server not at all.
- A simple request is one a 1999 HTML form could have made — which excludes every application/json call.
- Non-simple requests pay a preflight round trip, cacheable with Access-Control-Max-Age, which almost nobody sets.