CORS – Cross-Origin Communication in the Modern Web

So here is a situation you’re in. You just built a really cool blogging app using the latest and greatest JavaScript framework. You scaffold the application using Nuxt.js, build the components using Vue.js and use Vuex for state management. On the back-end side, you create an API using Node.js, FeathersJS and Express. Your back-end serves the blog post data to your front-end through an API.

Before pushing your code to a server, you decide to test whether everything is working correctly. Your Vue app is running at http://localhost:3000 while the API server is running at http://localhost:8000. You visit http://localhost:3000 in your web browser and you notice a bug. No data has been loaded from the API! Oh No!

You stay calm. You try to debug the situation. Like any good web developer you look in the browser’s DevTools for the answer.

And the first thing you see is the following error in the console.

Famous CORS Access-Control-Allow-Origin error
The infamously notorious CORS Policy Access-Control-Allow-Origin error

“Alright!”, you say to yourself, “There is probably a bug in the API. I’ll just use Postman to debug the API!”. You open up Postman and send a GET request to http://localhost:8000/api.

And here is what you get:

Successful API response in Postman
Successful API response

“What? The API is working? Then why is my JavaScript code failing?”

I don’t even remember the number of times I found myself in the same situation. I would always google the error and get some StackOverflow answer telling me to add CORS support. And I would do that by adding an Access-Control-Allow-Header:* to the HTTP response on the back-end. This would solve the issue. However I never really tried to understand the actual reason behind this error.

Recently, I decided to get to the bottom of this mystery once and for all. So I picked up CORS in Action and studied CORS in depth. In this post, I will walk you through what I understand to be the gist of the CORS mechanism.

Difference between the Browser and the Client

Now, before we start this discussion, I would like to clarify some technical terms.

The browser and the client are different things when we are talking about AJAX calls.

Shocking right? I know.

When we talk about the HTTP request/response cycle, we mostly use the term “client” to refer to the user’s browser initiating the request. However, in an AJAX call the the actual client is the JavaScript code that is making the call. The browser simply acts as a middleman between the client and the server.

Every time we initiate an HTTP request from JavaScript, be it through the XMLHttpRequest API or through the Fetch API, before that request is actually sent to the server, the browser intercepts the request and can modify it or refuse to send it to the server. And in some cases, refusing to send a request is because of the same-origin policy.

Before I explain the same-origin policy, let’s have a look at what an origin is and how it works.

What is an Origin?

The origin in an HTTP request comes from the webpage the JS script resides on. It is an HTTP header. Its value is the combination of a scheme, host and port from the URL. For example, in the following URLs

http://localhost:8000
https://api.alazierplace.com:3000/api/
https://alazier.place/2019/05/data-driven-testing-how-we-went-from-150-test-cases-to-1/

The URL scheme or protocol is the first part in the URL, before the ://. So in our case, the schemes would be http and https. The host is defined as the IP address or the domain, so in our case that would be localhost, api.alazierplace.com or alazierplace.com. And finally the port is the number after the colon, i.e. 8000 or 3000. In the case where no port is specified then the default HTTP ports are used i.e. 80 for HTTP URLs and 443 for HTTPS URLs. So the origin for the the URLs above are as below respectively:

  • http://localhost:8000
  • https://api.alazierplace.com:3000
  • https://alazier.place

Whenever a JavaScript script in a page makes an HTTP request to some API, the browser adds the Origin header (including other headers) before sending the request to the server.

Same-Origin Policy

The issue we face in the above scenario is due to the same-origin policy implemented by most browsers we use today. This policy dictates that a script or document loaded from one origin cannot freely interact or request data from other origins. By default a web page loaded from the origin https://alazier.place can only request resources (css, jpeg, or png files etc.) from the same origin or server i.e. https://alazier.place.

So, when you open my website, the first thing your browser will request is the HTML. The HTML contains a link to a CSS stylesheet which has the following URL

https://alazier.place/wp-content/cache/autoptimize/css/autoptimize_4fe4785dcf517d3a732df2418e95c09e.css

This request for the CSS file succeeds because the origin of the CSS file is the same as the web page requesting the file.

If the CSS file was hosted at another origin like for example

https://someotherwebsite.com/css/some-cool-stylesheet.css

Then the browser would not let that request succeed because it is requesting a resource from a different origin. This is a critical security measure which reduces the chances of malicious scripts and documents harming users and other websites. The important point to note is that, the same origin policy is a purely client side measure that is implemented in web browsers.

Now you may ask, that APIs usually are hosting on different origins than the main web app and the API calls and requests succeed, so how does that work?

Good question. The short answer to that is the CORS mechanism, which allows servers to control which origins can request resources from them and which cannot.

CORS – Cross-Origin Resource Sharing

CORS is short for Cross-Origin Resource Sharing. It is a web standard recommended by the W3C that enables web clients i.e. scripts running in the browser to access content from origins other than their own.

Web servers, through the use of HTTP response headers, can decide which origins to serve or reject, which HTTP methods to allow as well as other options. Web Browsers that implement the CORS policy will then check the CORS related response headers and act accordingly. CORS puts the control of the content in the hands of the servers.

Cross-Origin Request Cycle

So, the basic flow when making a cross-origin request is illustrated below:

Request/Response cycle for a CORS Request
Request/Response cycle for a CORS Request

Okay, so let’s break that down shall we?

  • The JS Client initiates an AJAX call.
  • The browser intercepts the request, and before sending this request to the server, it sends a pre-flight request. A pre-flight request is an HTTP Options request.
  • The response of the pre-flight request contains HTTP headers containing the CORS configuration of the server.
  • Once, the CORS policy has been verified through the pre-flight request, then the actual request is sent to the server.
  • The server returns the response which is then delivered to the client by the browser.

Pre-Flight Request

The interesting thing to note here is the pre-flight request. It is simply an HTTP OPTIONS request and its response contains HTTP headers that specify the CORS configuration. The pre-flight request is sent only once and is then cached by the browser.

The pre-flight request is not sent on every CORS request. It is sent only when the following criteria is met:

  • The request uses HTTP methods other than HEAD, GET or POST.
  • The Content-Type request header has a value other than application/x-www-form-urlencoded, multipart/form-data and text/plain.
  • The request sets headers other than Accept, Accept-Language and Content-Language.

These are collectively known as simple methods and simple headers.

CORS Headers

Here are the HTTP response headers used in CORS:

  • Access-Control-Allow-Origin – The most popular CORS header because it is the first to appear in most errors. It’s value can either be a single origin or the wildcard * value. If a single origin is listed, than then only scripts from that origin can request the server for resources. If a wildcard * value is supplied, than the server accepts requests from any origin. This is useful for creating public APIs.
  • Access-Control-Expose-Headers – This header lists headers that are white-listed and can be accessed by the client. The server sends these headers in the response.
  • Access-Control-Allow-Headers – This header lists which HTTP headers are allowed in the actual request.
  • Access-Control-Allow-Methods – This header lists which HTTP methods are allowed by the server when the resource is accessed.
  • Access-Control-Allow-Credentials – This header indicates whether the actual request can contain credentials i.e. cookies and other information that can be used to identify the user.
  • Access-Control-Max-Age – This header contains the number of seconds that the pre-flight request remains cached in the browser. If the time has expired then another pre-flight request is made on the next AJAX call.

Putting it all together

Okay, so we’ve seen what a CORS request is and how it works. But the question may arise, why is there a need for such a complex mechanism for making HTTP requests in JavaScript? We can easily make HTTP requests from any other language than why does JavaScript contain these restrictions?

The answer to that lies in the difference between the execution environments of the languages. Traditional programming languages such as Python, Ruby and Java etc. mostly run on servers or desktop apps. These environments are relatively more in control of the programmer. Even Node.js which runs JavaScript does not have CORS restrictions.

But most JavaScript programs are delivered to the users browser through a network. And they have to be executed in the user’s browser on the user’s device. That browser may contain some sensitive login info, or auth cookies or session data that could be exploited by JavaScript loaded by malicious pages.

The same-origin policy is implemented in browsers to prevent unrestricted access of other origins by any script. And the CORS policy, which is also implemented in browsers, enables a controlled and secure mechanism for JavaScript code to request certain resources while keeping the server in control of who to serve and who to not.

Caveats

As with everything, there are a few caveats and precautions that should be kept in mind while dealing with CORS configurations.

  • Pre-flight requests can introduce some problems with caching when the site is served behind a load balancer or proxy server. In such cases the origin of the request can vary, so pre-flight requests are not properly cached. This can be mitigated by adding the Origin header to the Vary header.
  • The same-origin and the CORS policies are implemented by web browsers. If the user is using a web browser which does not support these mechanisms, then the CORS request will work as a normal HTTP request.
  • CORS is not the only way to do cross-origin communication. JSONP, postMessage and server side requests are all way in which a browser’s CORS policy can be bypassed.

Tribute

I read a complete book just to prepare this blog post. And that book was CORS in Action by Monsur Hossain. It is an exceptional book and the code samples in it explain CORS in an excellent manner.

To all the web developers looking to take a deeper dive into cross-origin communication, definitely give Monsur Hossains’s book a read. It is well worth the investment.

2 thoughts on “CORS – Cross-Origin Communication in the Modern Web”

  1. A very eye-opening piece. Thank you so much 😊. I saw some errors in my console with CORS shining in them and decided to take them for granted, but after reading this piece, I’m intrigued by the whole system and will take a dive into CORS in action. But under Access-Control-Allow-Origin, fourth sentence, there’s a typo, a than* doing the work of a then*(was so into the piece so that part had me stuck a little). But once again, thanks

Comments are closed.