8 min
  • HTTP
  • APIs

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.

text
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.

Loading the HTTP Explorer simulation…

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.

Check yourself
A page at https://app.example makes a cross-origin GET and the console reports a CORS error. Did the server receive the request?

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.

RequestSimple?Because
GET with no custom headersYesA link could do it
POST of application/x-www-form-urlencodedYesA form could do it
POST of application/jsonNoNo form could produce that content type
Anything with AuthorizationNoNot a header a form could set
PUT, PATCH, DELETENoForms 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.

Predict what happens next
A single-page app makes forty JSON API calls to another origin during a page load. How many preflights?

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.

Take it further