Blog

- September 27, 2016

You’ve probably already seen errors like the following one in the JavaScript console of your browser, when trying to execute AJAX requests:

At first sight, it’s difficult to understand what’s happening and find out where the problem is coming from. What we can be sure of is that the errors are related to the CORS mechanism (Cross-Origin Resource Sharing). Throughout this article, we will describe how to understand what is happening and how to fix problems like these.

Understanding CORS

With CORS, the remote Web application chooses if the request can be served. The CORS specification distinguishes two distinct use cases:

  • Simple requests. This use case applies if we use HTTP GET, HEAD and POST methods. In the case of POST methods, only content types with the following values are supported: text/plain, application/x-www-form-urlencoded and multipart/form-data.
  • Preflighted requests. When the ‘simple requests’ use case doesn’t apply, a first request (using the HTTP OPTIONS method) is made to check what can be done in the context of cross-domain requests.

These two scenarios were created by the developers of CORS to take into account old Web servers that don’t support the specification. For more details, you could have a look at this great answer on the StackOverflow website

Notice that if you add authentication to the request using the Authentication header, simple requests automatically become preflighted ones.

Client and server exchange a set of headers to specify behaviors regarding cross-domain requests. Let’s have a look at them now. We will then describe how they are used in both use cases. Let’s start with headers for requests:

  • Origin: this header is used by the client to specify which domain the request is executed from. The server uses this hint to authorize, or not, the cross-domain request.
  • Access-Control-Request-Method: within the context of preflighted requests, the OPTIONS request sends this header to check if the target method is allowed in the context of cross-domain requests.
  • Access-Control-Request-Headers: within the context of preflighted requests, the OPTIONS request sends this header to list the headers will be accepted by the server for the target method in the context of cross-domain requests.

Let’s continue with headers for responses:

  • Access-Control-Allow-Credentials: this specifies if credentials are supported for cross-domain requests.
  • Access-Control-Allow-Methods: the server uses this header to inform which methods are authorized in the context of the request. This is typically used in the context of preflighted requests.
  • Access-Control-Allow-Origin: the server uses this header to inform which domains are authorized for the request.
  • Access-Control-Allow-Headers: the server uses this header to inform which headers are authorized in the context of the request. This is typically used in the context of preflighted requests.

We listed the main CORS headers. More details about CORS principle can be found on the Mozilla Developer Network.

Now that we described the foundations, let’s see these principles in action within browsers.

Debugging CORS in the browser

To be able to find out what’s happening, we need to leverage the tooling of the browser to see the error message and the exchanged requests and responses. Without these hints, it’s difficult, even impossible, to understand the error.

Most of the time, developers work with a REST client like Restlet Client to test the interactions with the Web API / REST service. At this level, requests work perfectly within the browser since Chrome extensions can specify particular permissions in their configuration file (manifest.json). The webRequest one allows you to execute any request in the browser through an extension. Here is a sample of configuration:

{
  "manifest_version": 2,
  "short_name": "My Chrome extension",
  (...)
  "permissions": [
    (...)
    "webRequest",
    "webRequestBlocking",
    "cookies",
    "<all_urls>",
    (...)
  ],
  (...)
}

The traditional client-side applications can’t do this and and must respect the CORS rules when executing cross-domain requests. Be aware that such mechanisms are internally handled by the browser. There is nothing to do at the level of the application executed within the browser.

To make things work, the CORS mechanism needs to be implemented the right way, i.e. requests and responses with the right headers. We will now detail all these exchanges.

Simple requests

As described previously, simple requests are based on single requests, i.e. no additional OPTIONS requests. This case concerns HTTP GET, HEAD methods but also the POST one in some cases.

When the browser detects that the AJAX request isn’t on the same domain as the current page, it automatically sends an Origin header to the server. This is supposed to enable the CORS support on the server for the request, and the browser expects to receive CORS headers in the response. Here is a sample:

fix-cors-problems_1

Note: two requests with the same host but not the same port aren’t considered to be on the same domain.

We will have the same behavior with a POST request that sends a form payload (application/x-www-form-urlencoded content type):

fix-cors-problems_2

Imagine that the server doesn’t return the CORS headers (for example, the Access-Control-Allow-Origin one), the browser will show the following errors in the JavaScript console:

fix-cors-problems_3

To debug the problem, take a look in the Network tab. We can’t find the Access-Control-Allow-Origin header in the “Response Headers” area.

fix-cors-problems_4

The problem isn’t in the client application but in the server application. To fix it, we need to enable CORS support at the server level.

Simple requests are the simplest case for cross domain requests. Let’s now have a look at the preflighted ones.

Preflighted requests

Preflighted requests involve an additional OPTIONS request that is executed under the hood by the browser before the target one. We can switch to the preflighted mode very easily. For example:

  • Change the content type of our previous POST request from application/x-www-form-urlencoded to application/json.
  • Use a HTTP method like PUT, PATCH or DELETE
  • Add a HTTP header like Authorization for authentication.

For this reason, the difference between simple and preflighted requests can often be unclear. This could mainly be because of the support of the POST method. For example, your application can work for a submission of a form payload (application/x-www-form-urlencoded content type) but not with a JSON one if the server doesn’t handle OPTIONS methods correctly.

Here is a sample of a preflighted request, with the submission of a JSON payload, using the POST method:

fix-cors-problems_5

Here are the details of the OPTIONS request:

fix-cors-problems_6

It’s important to note that we can also have preflighted requests in the case of a GET method. That’s the case if we add an Authorization header to the GET request described in the previous section.

fix-cors-problems_7

In this context, we can have several potential errors. Let’s take a look at the most common ones and how to fix them.

CORS requests / Options are not enabled

The first error is that OPTIONS methods aren’t supported by the server because it doesn’t support CORS at all, or its support is minimal and doesn’t support preflighted requests. In this case, we will see an error like this in the JavaScript console:

fix-cors-problems_8

We can notice that the message mentions the lack of the CORS Access-Control-Allow-Origin in the response and the 405 HTTP status code. This status code is a useful hint to understand that the server doesn’t support OPTIONS requests. Let’s have a look at the Network tab:

fix-cors-problems_9

We can determine that we would encounter the same problem with a 4xx status code. The way to fix this problem consists of:

  • Add the support of the OPTIONS method so that CORS preflight requests are valid
  • Add the Access-Control-Allow-Origin header in your response so that the browser can check the request validity

Request headers are not supported

Another error could be that headers we want to use in our request (for example, Content-Type or Authorization) aren’t enabled in the CORS preflighted response using the Access-Control-Allow-Headers header. In this case, we will have the following error:

fix-cors-problems_10

In the corresponding preflighted request, we can see that no Access-Control-Allow-Headers header is returned in the response whereas the target request wants to use the Content-Type header:

fix-cors-problems_11

To fix this problem, simply add the corresponding header on the server side when handling the OPTIONS method. We will then have the following requests:

fix-cors-problems_12 fix-cors-problems_13

Unauthenticated OPTIONS request

Another common problem is associated with the use of the Authorization header. The unusual thing is that this header isn’t sent within the preflighted request. So if the server application tries to authenticate all requests, even the preflighted ones, we will have an “unauthorized” error with the 401 status code:

fix-cors-problems_14

The server needs to be fixed so it doesn’t try to authenticate preflighted requests. After having done that, we would see the following requests:

fix-cors-problems_15

fix-cors-problems_16

Fixing problems

In this section, we will describe the possible solutions, according to the context, to fix problems related to CORS.

Updating the server

Most of the time, the simplest way to fix the issue is to update the server. Most of server-side technologies provide support to configure CORS quickly. For example, with Node and ExpressJS, it only consists of installing the CORS middleware and using it when initializing the Express application:

var express = require('express')
  , cors = require('cors')
  , app = express();

app.use(cors());

(...)

There are cases where the target service is a third-party one and the update cannot be made to support CORS. Several approaches need now to be considered.

JSONP

The first one is JSONP. This technique has been used for a long time for online services and cross domain requests, but the remote services need to use it. It consists of dynamically adding a script element in the HTML page to execute the request. When the response is received, the corresponding code is parsed and executed as JavaScript.

For this reason, the response content must follow the following format:

callbackName([ { name: ‘some value' }, { name: ‘some other value' }])

The name of the callback is provided as a parameter of the request. Most of the time and by convention, the name of the callback parameter is callback or c. Here is a sample request:

http://somehost/resource/someid?callback=callbackName

We can notice that such approach has drawbacks and restrictions, since only GET methods are supported and payload can only be provided as query parameters.

Service proxy

The last remaining possibility is to add a proxy on the server side within your application but it requires a bit of work. We can however notice that Ionic supports proxy through its ionic.config.json configuration file:

(...)
 "proxies": [{
    "path": "/yelp/v3/businesses",
    "proxyUrl": "https://api.yelp.com/v3/businesses"
 }]
(...)

CORS and Web APIs

Since web APIs target communication between applications, they support CORS out of the box most of the time. That’s the case with Restlet Cloud our platform to implement and host web APIs.

For example, for a request to get all contacts of a store using a web API (created using the Quickstart wizard), Cloud will automatically handle the corresponding preflighted request, as described below:

fix-cors-problems_17

fix-cors-problems_18

An interesting feature to mention is the support of CORS for files as most of static web servers don’t support this specification when serving files. Restlet Cloud allows us to expose a filestore within a web API and have access to its CORS support in this context.

Conclusion

As described throughout the article, CORS targets cross-domain requests and has specific mechanisms that can be tricky to debug. This is mainly because of preflighted requests that are used in some cases and involved an additional OPTIONS request.