TIL: Preflight requests in CORS
Cross-origin requests are only preflighted if the browser thinks that the request might cause a server-side mutation.
If you make a cross-origin
fetch request, it gets classified
into one of two categories: “simple” or “preflighted”.
Simple requests are ones where it is (conventionally) safe to piggyback off the
actual request to determine the CORS policy. I say “conventionally” because REST/HTTP
does not enforce that you only read on
GET requests, or that
PUT will create-or-replace,
there are only conventions.
Anyway, if your request has a
GET method, the browser can assume that the
origin server will not do any mutations when processing this request, so it just
looks for the
Access-Control-Allow-Origin header in the actual request’s response,
AKA CORS policies are inferred from the actual request. This is considered safe
to do, and it saves one round-trip.
DELETE though, sending the actual request might trigger server-side
mutations (like creating or deleting a row in a database), so the browser needs to
make sure that the origin making this request is allowed to do so before making
the actual request. It cannot piggyback off the actual request to infer CORS policies
because doing so defeats the purpose of CORS — the origin was able to mutate the host
without explicit permission. Sure, the origin might not get to see the host’s response,
but the damage is already done. This is why the browser must “preflight” the actual
request, AKA send a separate (
OPTIONS) request to explicitly ask the host if the
request it intends to send is allowed. This (preflight)
OPTIONS request has the
Access-Control-Request-Method that describes what method the actual request
will be, and
Access-Control-Request-Headers, that lists the headers the actual
request will have. Only if the host server says that the origin is allowed to
send a request with the method in
Access-Control-Request-Method and the headers
Access-Control-Request-Headers, is the actual request sent out.
The decision to preflight a request lies entirely in the hands of the client (which
is a web browser in most cases). It tries its best to not send an additional request,
within the bounds of the CORS contract. Unfortunately, this decision is not based
on the request method alone, there are edge cases that may cause even a
to be preflighted.
MDN’s CORS documentation is actually a great read, and expands on how the browser decides if a request needs to be preflighted.