One thing I’ve seen experienced JavaScript developers struggle with is making cross-domain requests. Libraries like jQuery will handle all of the complexities of this and gracefully degrade to other technologies as much as possible, but it is important for JS devs to know what is going on under the covers. That’s where this post comes in.
Background
HTTP requests from Javascript are traditionally bound by the Same Origin Policy, which means that your ajax requests must have the same domain and port. The common ways to get around this are JSON-P, Proxying and message passing via <iframe>
s. These all have their quirks, but the thing they generally have in common is legacy browser support.
CORS stands for Cross-Origin Resource Sharing. It is a more robust way of making cross-domain requests supported by all but the lowest grade browsers (IE6 and IE7).
Why you should use CORS
Compared to proxying, the significant advantage of CORS is not having another system component, possibly complicating the app.
It has a few big advantages over JSON-P as well:
- It supports HTTP verbs other than GET
- Error handling with JSON-P is tricky or impossible. CORS has access to the HTTP status and the response body just like XHR does
- CORS supports many types of authorization (like Basic Auth or OAuth). JSON-P only supports cookies.
IE8 and IE9 support simple CORS requests (via XDomainRequest) and all major browsers (including mobile) have supported it for quite some time. Since IE8 is becoming the new lowest common denominator, we can and should start using this awesome technology. See Browser support for CORS according to MDN.
How CORS works
To show you how it works, I’ve come up with the following scenario. I’m going to omit the UI building and focus on the Javascript and the HTTP requests.
Suppose we are building a one-page webapp on foo.com that allows us to manage a product list through an API hosted on another domain.
First, we need to get a list of products by making this request from our website, http://foo.com:
GET http://api.foo.com/products
This is an example of a “simple” request (more on this later, but in short this means the browser doesn’t have to add another request to get approval for the desired request). Here is a simple JS snippet to do this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// DO NOT COPY THIS CODE, ERROR HANDLING AND IE9-
// SUPPORT HAVE BEEN OMITTED FOR BREVITY!
var req = new XMLHttpRequest();
// Feature detection for CORS
if ('withCredentials' in req) {
req.open('GET', 'http://api.foo.com/products', true);
// Just like regular ol' XHR
req.onreadystatechange = function() {
if (req.readyState === 4) {
if (req.status >= 200 && req.status < 400) {
// JSON.parse(req.responseText) etc.
} else {
// Handle error case
}
}
};
req.send();
}
CORS uses HTTP headers to control access to the remote resource. Behind the scenes, the browser is adding a request header, Origin: http://foo.com
which could be used by the API to restrict access. The API will generally send back a CORS-specific header with the response, Access-Control-Allow-Origin: http://foo.com
which denotes the origin domains allowed to make requests to the API. A “*” would mean all domains are allowed.
Simple vs. Preflighted requests
Now suppose we want to add a new product, but we want to use an Ajax request to keep the user on our the same page (instead of using a form). We want to make a request like this:
POST '{"name": "Awesome Widget", "price": "13.37"}' http://api.foo.com/products
If we want to send the data with a Content-Type of application/json, this would turn our request into a “Preflighted” request. In this case, CORS adds an extra step to the request. This is required by the spec in any of the following circumstances:
- Uses an HTTP verb other than GET or POST
- Custom headers need to be sent (e.g.
X-API-Key: foobar
) - The request body has a MIME type other than
text/plain
These requests have to be “preflighted”, meaning that the browser sends OPTIONS http://api.foo.com
request to the URL, and the server must respond with a response that basically approves the actual request you want. Here’s what the sequence of HTTP requests and responses might look like (some irrelevant headers omitted):
=> OPTIONS https://api.foo.com/products
- HEADERS -
Origin: http://foo.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Api-Key
<= HTTP/1.1 204 No Content
- RESPONSE HEADERS -
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: Api-Key
Access-Control-Allow-Origin: http://foo.com
Content-Length: 0
=> POST https://api.foo.com/products
- HEADERS -
Origin: http://foo.com
Access-Control-Request-Method: POST
Content-Type: application/json; charset=UTF-8
<= HTTP/1.1 200 OK
- RESPONSE HEADERS -
Access-Control-Allow-Origin: http://foo.com
Content-Type: application/json
Content-Length: 58
- RESPONSE BODY -
{"id": "123", "name": "Awesome Widget", "price": "13.37"}
For the sake of IE8 and IE9 support, we want to keep requests “simple”, but if this is a mobile app without that restriction, we would write it like this:
1
2
3
4
5
6
7
8
9
10
var req = new XMLHttpRequest();
var body = // JSON.stringify(productData) or something
if ('withCredentials' in req) {
req.open('POST', 'http://api.foo.com/products', true);
req.setRequestHeader('Content-Type', 'application/json');
req.setRequestHeader('Api-Key', 'foobar');
req.onreadystatechange = handleResponse;
req.send(body);
}
I did not add an example using “withCredentials”, but the important thing to know about it is that cookies or certain auth headers aren’t sent unless you set req.withCredentials = 'true';
. Use this for authenticated CORS requests.
Here is more robust Javascript code (no fallbacks, though) if you want to see it. Please fork and improve this gist if you see any issues.
Conclusion
Since IE7 is being phased out and we’re build more mobile webapps that have cross-domain capabilities, CORS is the most robust solution for making cross-domain requests with Javascript for the foreseeable future. It’s the only good way to handle RESTful APIs with JS.
If you want more detail on CORS, I recommend reading MDN Docs on CORS and the spec.