CORS, Preflight request and OPTIONS Method

didof

Francesco Di Donato

Posted on April 10, 2022

CORS, Preflight request and OPTIONS Method

Premise

This post is intended to be a light reading with the purpose to give a minimum of context and instill some curiosity towards a topic often considered opaque - CORS are a simple HTTP-header mechanism that every web developer can easily understand.


First, you need to be aware of the Same-origin Policy. It is a browser built-in mechanism which restricts how a script on one origin interacts with a resource on another.

Same-origin 🏠🏠

Let's start with the simplest case. Both the server that holds the resource and the client that requests it reside under the same origin (<scheme>://<hostname><port>).

The server is exposed at http://localhost:3000 and responds to HTTP GET / by returning an HTML file.

For the sake of brevity, the code examples only show what I want to focus on. If something is not clear, use the various Accompanying source code (same-origin) you'll find.

server.js


server.get("/", (_, reply) => {
  reply.sendFile("index.html");
});

server.get("/me", (_, reply) => {
  reply.status(200).send({ nickname: "didof" });
});


Enter fullscreen mode Exit fullscreen mode

The HTML arrived on the client asks the server on the same origin for the resource associated with the route /me and shows the nickname on the screen.

index.html


<p>Nickname: <output></output></p>

<script>
    // This runs on http://localhost:3000
    fetch("/me") // fetch("http://localhost:3000/me")
    .then((res) => res.json())
    .then((res) => {
        document.querySelector("output").textContent = res.nickname;
    });
</script>


Enter fullscreen mode Exit fullscreen mode

The payload is accessible as same-origin policy is respected. No need for CORS, no need for preflight request.


Cross-origin 🏠🏭

Now the client is exposed on http://localhost:8000 while the server with the resource is exposed on http://localhost:3000. Even if scheme and hostname are the same, the ports are different - therefore they are two different origins.

In the real world, the following concept is found when the client on https://mywebsite.com requires a resource to http://api.foo.it/bar

Accompanying source code (cross-origin)

index.html


<script>
// This runs on http://localhost:8000
const request = new Request("http://localhost:3000/me", {
  method: "GET",
});

fetch(request)
.then((res) => res.json())
.then((res) => { ... })
</script>


Enter fullscreen mode Exit fullscreen mode

As you can guess, you have a CORS error.

Access to fetch at 'http://localhost:3000/me' from origin 'http://localhost:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. GET http://localhost:3000/me net::ERR_FAILED 200

The error in the console clearly explains the problem. Let's fix it. On the server, we manage CORS.

server.js


server.register(fastifyCors, {
  origin: "http://localhost:8000",
});


Enter fullscreen mode Exit fullscreen mode

I use fastify here, but any framework offers its own way of managing CORS. It's just headers added to the response, nothing more - you can of course append them manually.

Now, the response will include the access-control-allow-origin and the value will be http://localhost:8000.

access-control-allow-origin: http://localhost:8000

The server is basically giving permission to the browser which issued the request to unfold the response.


Preflight Request 🏠✉️🏭

There are two types of CORS request:

  • Simple request
  • Preflight request

Which is used is determined by the browser. Up to this moment the client has carried out simple requests because they fit the criteria.

For the purpose of this post, you only need to know that to force the browser make a preflight request you just need to add to the request a custom header.

Accompanying source code (cross-origin-preflight)

index.html


<script>
const headers = new Headers();
headers.append("X-CUSTOM-HEADER", "foo");

// This runs on http://localhost:8000
const request = new Request("http://localhost:3000/me", {
  method: "GET",
  headers,
});

fetch(request)
  .then((res) => res.json())
  .then((res) => { ... })


Enter fullscreen mode Exit fullscreen mode

And that's enough for the browser to fire two requests instead of one.

request 1 = localhost, request 2 = me (preflight), request 3 = me (actual request)

The method used is OPTIONS, which is interpreted by the server as a query for information about the defined request url.

The browser also appends some headers to the preflight request. Access-Control-Request-Headers and Access-Control-Request-Method with their relative values.
Access-Control-Request-Headers: x-custom-header, Access-Control-Request-Method: GET

The browser is asking permission to the server to make a GET request which has among the various headers also a certain X-CUSTOM-HEADER.

This particular server, configured to be extremely permissive, responds with access-control-allow-headers whose value mirrors the requested one and access-control-allow-methods which tells that all methods are allowed.

access-control-allow-headers: x-custom-header, access-control-allow-methods: GET,HEAD,PUT,PATCH,POST,DELETE

It's giving a big thumbs up 👍. The browser then can perform the wanted request.

Obviously, if for some reason the server was instructed not to allow the presence of the custom header you would get an error.

Access to fetch at 'http://localhost:3000/me' from origin 'http://localhost:8000' has been blocked by CORS policy: Request header field x-custom-header is not allowed by Access-Control-Allow-Headers in preflight response.


As mentioned, this is but a simple chat. If interested, below are some references to go further. I 💖 the H of HTML!

Resouces 🕳️🐇

Get in touch

💖 💪 🙅 🚩
didof
Francesco Di Donato

Posted on April 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related