Thomas Wang

From journeyman to master.


A practical guide to CORS

A practical guide for things you need to know about enabling Cross-Origin Resource Sharing (CORS).

Why we need CORS

Web pages are under the restrictions of same-origin policy, which means scripts contained in a web page cannot access data in another page with different origin. For example, http://www.example.com:81/dir/other.html cannot be accessed by http://www.example.com/dir/page.html because the port is different. You can find more detailed explanations on wiki.

However, there are some cases we want to enable cross-origin resource sharing.

Demo setup

So let's get started. I've prepared a simple KOA server to demonstrate the idea. You can find the demo on Github and follow the tag for each stage.

git clone git@github.com:xg-wang/cors-guide.git
cd cors-guide
npm i
npm start

The server runs at http://localhost:3000/ and frontend runs at http://localhost:8000/

01. CORS fail

First we simply retrieve the data through API running on port 3000:

fetch('http://localhost:3000/')
.then(res => res.text())
.then(res => responseElement.innerHTML = res)
.catch(err => responseElement.innerHTML = err)

Click the button to send the request, you could see the returned response is "TypeError: Failed to fetch".

fail

Now open your dev console, you can find the following error:

Failed to load http://localhost:3000/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Because our frontend is hosted on port 3000, and server is running on port 8000, the request from browser is restricted by the same-origin policy. To fix it, simply add a header to the response in our server code:ยท

Access-Control-Allow-Origin: http://localhost:8000

Now click the button again and you should be able to see the response from server!

Nails it

Open up the dev server and check the request and response, you can find the Origin: http://localhost:8000 header in the request. This is added by the browser automatically.
In the response, we have the header we just added: Access-Control-Allow-Origin: http://localhost:8000. This allows our http://localhost:8000 to have access to the API endpoint. You may also specify "*" as a wildcard, thereby allowing any origin to access the resource.

02. Preflight

This is not the end of story, let's try to add some header to our request as 'Content-Type': 'application/json'.

Send the request again and it fails. The console has the following error:

Failed to load http://localhost:3000/: Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

What is a preflighted request/response?

Open up the dev tools network tab, the request sent by the browser is not the request we created by JavaScript. It is called a preflighted request and has the following Headers:

Request Method: OPTIONS
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET

Together with the Origin: <origin> header they are all we need for the request. In any access control request, the Origin header is always sent.
And Access-Control-Request-Headers might also be a comma separated array.

These headers are used to tell the server what the following actual CORS request contains. Here we are asking the server to allow the Content-Type header and GET method.

After the browser receives the legit preflight response, the actual CORS request is sent.

MDN example for preflight request

MDN example for preflight request

Why there was no preflight request?

In the previous section, our request went well and there is no CORS preflight - it is called a 'Simple Request'.
MDN has a comprehensive description about this.
To summarize, a simple request is a GET|HEAD|POST method, and only contains 'CORS-safelisted request-header'

A CORS-safelisted request-header is a header whose name is a byte-case-insensitive match for one of

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type and whose value, once extracted, has a MIME type (ignoring parameters) that is > application/x-www-form-urlencoded, multipart/form-data, or text/plain

or whose name is a byte-case-insensitive match for one of

  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

and whose value, once extracted, is not failure.

By CORS spec, a simple request won't trigger the preflight request.

How to fix it?

To grant access to the resource, we need to set corresponding headers in the response for the preflight request.

Access-Control-Allow-Origin: http://localhost:8000
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Content-Type

Several things to notice:

There are 3 more access control headers you can set:

03. Credentials

By default, in cross-site XMLHttpRequest or Fetch invocations, browsers will not send credentials (HTTP cookies and HTTP Authentication information).
But they both have option flag to set. Take Fetch for example, there is a credentials option:

The request credentials you want to use for the request: omit, same-origin, or include. To automatically send cookies for the current domain, this option must be provided. Starting with Chrome 50, this property also takes a FederatedCredential instance or a PasswordCredential instance.

If we add this option and send the request:

fetch('http://localhost:3000/', {
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
}).then(res => res.json())
.then(res => responseElement.innerHTML = res)
.catch(err => responseElement.innerHTML = err)

The following error will popup in your dev console:

Failed to load http://localhost:3000/: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. Origin 'http://localhost:8000' is therefore not allowed access.

Fix

Remember the Access-Control-Allow-Credentials?
Set this header in both preflight and request: Access-Control-Allow-Credentials: true. Refresh, send again and it succeeds.

Some more things to notice:

If the server specifies an origin host rather than "*", then it could also include Origin in the Vary response header to indicate to clients that server responses will differ based on the value of the Origin request header.

A more persuasive reason can be found here.

One more thing

In the Fetch API, you can set Request.mode

The mode read-only property of the Request interface contains the mode of the request (e.g., cors, no-cors, same-origin, or navigate.) This is used to determine if cross-origin requests lead to valid responses, and which properties of the response are readable.

Set this to 'cors' when sending CORS request, more details can be found in the spec.

Summary

In this post we discussed the use cases of CORS and how we can enable it by building a simple API step by step. You can find the full demo on Github.

For conclusion, to enable CORS for a general CORS request, you need to add the following headers: