Cross-domain Ajax with Cross-Origin Resource Sharing
A couple of years ago, web developers were banging their head against the first wall in Ajax: the same-origin policy. While we marveled at the giant step forward enabled by cross-browser support for the XMLHttpRequest object, we quickly bemoaned the fact that there was no way to make a request to a different domain from JavaScript. Everyone setup proxies on their web sites, which was the onset of a new host of open redirect problems, as a way to get around the restriction. Although developers were working around this limitation using server-side proxies as well as other techniques, the community outcry was around allowing native cross-domain Ajax requests. A lot of people are unaware that almost all browsers (Internet Explorer 8+, Firefox 3.5+, Safari 4+, and Chrome) presently support cross-domain Ajax via a protocol called Cross-Origin Resource Sharing.
Cross-Origin Resource Sharing (CORS)
Cross-Origin Resource Sharing (CORS) is a W3C Working Draft that defines how the browser and server must communicate when accessing sources across origins. The basic idea behind CORS is to use custom HTTP headers to allow both the browser and the server to know enough about each other to determine if the request or response should succeed or fail.
For a simple request, one that uses either GET or POST with no custom headers and whose body is text/plain, the request is sent with an extra header called Origin. The Origin header contains the origin (protocol, domain name, and port) of the requesting page so that the server can easily determine whether or not it should serve a response. An example Origin header might look like this:
Origin: http://www.nczonline.net
If the server decides that the request should be allowed, it sends a Access-Control-Allow-Origin header echoing back the same origin that was sent or “*” if it’s a public resource. For example:
Access-Control-Allow-Origin: http://www.nczonline.net
If this header is missing, or the origins don’t match, then the browser disallows the request. If all is well, then the browser processes the request. Note that neither the requests nor responses include cookie information.
All of the previously mentioned browsers support these simple requests. Firefox 3.5+, Safari 4+, and Chrome all support usage through the XMLHttpRequest object. When attempting to open a resource on a different origin, this behavior automatically gets triggered without any extra code. For example:
var xhr = new XMLHttpRequest();
xhr.open("get", "http://www.nczonline.net/some_resource/", true);
xhr.onload = function(){ //instead of onreadystatechange
//do something
};
xhr.send(null);
To do the same in Internet Explorer 8, you’ll need to use the XDomainRequest object in the same manner:
var xdr = new XDomainRequest();
xdr.open("get", "http://www.nczonline.net/some_resource/");
xdr.onload = function(){
//do something
};
xdr.send();
The Mozilla team suggests in their post about CORS that you should check for the existence of the withCredentials property to determine if the browser supports CORS via XHR. You can then couple with the existence of the XDomainRequest object to cover all browsers:
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("get", "http://www.nczonline.net/");
if (request){
request.onload = function(){
//do something with request.responseText
};
request.send();
}
The XMLHttpRequest object in Firefox, Safari, and Chrome has similar enough interfaces to the IE XDomainRequest object that this pattern works fairly well. The common interface properties/methods are:
abort()– use to stop a request that’s already in progress.onerror– use instead ofonreadystatechangeto detect errors.onload– use instead ofonreadystatechangeto detect successes.responseText– use to get contents of response.send()– use to send the request.
Preflighted requests
CORS allows the use of custom headers, methods other than GET or POST, and different body content types through a transparent mechanism of server verification called preflighted requests. When you try to make a request with one of the advanced options, a “preflight” request is made to the server. This request uses the OPTIONS method and sends the following headers:
Origin– same as in simple requests.Access-Control-Request-Method– the method that the request wants to use.Access-Control-Request-Headers– (Optional) a comma separated list of the custom headers being used.
Example assuming a POST request with a custom header called NCZ:
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
During this request, the server can determine whether or not it will allow requests of this type. The server communicates this to the browser by sending the following headers in the response:
Access-Control-Allow-Origin– same as in simple requests.Access-Control-Allow-Methods– a comma separated list of allowed methods.Access-Control-Allow-Headers– a comma separated list of headers that the server will allow.Access-Control-Max-Age– the amount of time in seconds that this preflight request should be cached for.
Example:
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
Once a preflight request has been made, the result is cached for the period of time specified in the response; you’ll only incur the cost of an extra HTTP request the first time a request of this type is made.
Firefox 3.5+, Safari 4+, and Chrome all support preflighted requests; Internet Explorer 8 does not.
Credentialed requests
By default, cross-origin requests do not provide credentials (cookies, HTTP authentication, and client-side SSL certificates). You can specify that a request should send credentials by setting the withCredentials property to true. If the server allow credentialed requests, then it responds with the following HTTP header:
Access-Control-Allow-Credentials: true
If a credentialed request is sent and this header is not sent as part of the response, then the browser doesn’t pass the response to JavaScript (responseText is an empty string, status is 0, and onerror() is invoked). Note that the server can also send this HTTP header as part of the preflight response to indicate that the origin is allowed to send credentialed requests.
Internet Explorer 8 doesn’t support the withCredentials property; Firefox 3.5, Safari 4, and Chrome all support it.
Conclusion
There is a lot of solid support for cross-domain Ajax in modern web browsers, yet most developers are still unaware of this powerful capability. Usage requires just a little bit of extra JavaScript work and a little extra server-side work to ensure that the correct headers are being sent. IE8′s implementation lags a bit behind the others in terms of allowing advanced requests and credentialed requests, but hopefully support for CORS will continue to improve. If you’d like to learn more, I highly suggest checking out Arun Ranganathan’s examples page.
Update (25 May 2010): Fixed typo in example code.
Update (27 May 2010): Removed trailing slash from Origin headers.
Disclaimer: Any viewpoints and opinions expressed in this article are those of Nicholas C. Zakas and do not, in any way, reflect those of my employer, my colleagues, Wrox Publishing, O'Reilly Publishing, or anyone else. I speak only for myself, not for them.
Both comments and pings are currently closed.




32 Comments
You have a minor typo in your code example:
} else if (typeof XDomainRequest != “undefined”){
xhr = new XDomainRequest();
xdr.open(method, url);
should be xhr instead of xdr:
} else if (typeof XDomainRequest != “undefined”){
xhr = new XDomainRequest();
xhr.open(method, url);
Peter Foti on May 25th, 2010 at 10:40 am
Great, so we can do cross-domain Ajax with all (only) the modern browsers… what about FF3, Saf3, IE6/7, and all versions of Opera?? That still represents a fairly wide-spread section of the browser market and visitors.
Clearly, there must be some rational fallbacks still necessary, as those browsers without cross-domain ajax support natively can’t yet be ignored.
The problem is, the alternatives are often *very* hacky, and generally include writing code that is almost entirely different than if you were working with the XHR object (like JSON-P, window.name, iframe proxies, etc). I guess the smoothest approach is probably to implement conditional code that either makes cross-domain ajax calls directly if supported, or falls back to making Ajax requests through a local server proxy instead. Still, I bet that will be some hacky code. And server proxies suck in general.
I’m also a little disappointed that it seems a little complex to implement special server logic to receive the preflight requests and interpret and send back appropriate responses under various scenarios. I think I prefer the Flash/Silverlight approach of a single server policy file that you simply create and deploy once, instead of server logic having to run every time. I’d imagine for most readers that such logic may be a little too complex to feasibly roll out.
———————-
One other option readers may want to consider (although it definitely has its drawbacks too) is using flXHR, which is a cross-domain Ajax solution that uses the identical interface to native XHR (so your code probably doesn’t have to change hardly at all) but which uses an invisible .SWF file in the background to make the calls. flXHR makes cross-domain XHR work like it should have worked in all those old browsers, and keeps with the standardized XHR API instead of IE’s approach to fork to a new API.
In fact, there are plugins for several of the major frameworks which can adapt them to transparently use flXHR for cross-domain Ajax calls — since the API is the same the framework doesn’t know the difference.
One drawback to be aware of is that flash can only communicate with a server if it publishes a crossdomain.xml policy file. So, if you’re trying to call to a server you don’t control, and they don’t have a policy file, you’re out of luck. But you’d similarly be out of luck if they didn’t support these custom headers as this article describes. So, it’s probably a moot condition.
I guess at this point, I’d recommend doing feature detection for “withCredentials” (for FF3.5+, Chrome, and Saf4+), and then instead of using IE’s XDR (which I dislike for many reasons), simply falling back to using flXHR for all IE and for all other older browsers. Then the cutoff for users is, if you have an old browser AND you don’t have flash installed, well then they’re left out. The good news is, that’s a VERY small percentage of the population at that point that has an old browser and no flash.
Kyle Simpson on May 25th, 2010 at 11:30 am
Thanks for a useful article. There is a typo in the CreateCORSRequest function.
xhr = new XDomainRequest();
xdr.open(method, url);
should be
xhr = new XDomainRequest();
xhr.open(method, url);
Roger Roelofs on May 25th, 2010 at 1:49 pm
@Roger/Peter – thanks, I’ve fixed the example code.
@Kyle – thanks for reminding me that I need to put a size limit on comments.
Kidding aside, the CORS behavior isn’t very complicated at all. You can argue that the cross-domain Flash approach is simpler, but its downside is that it relies on Flash. Of course, this article wasn’t about what to do in older browsers (I’d probably just use a JSON-P interface), it was about raising the level of awareness of what’s available in modern browsers.
Nicholas C. Zakas on May 25th, 2010 at 9:41 pm
Isn’t using xhr.onload etc. instead of xhr.onreadystatechange simply the matter of XHR 2.0 (draft) instead of older XHR 1.0?
Jakub Narębski on May 26th, 2010 at 4:28 am
@Nicholas — the problem I have with an article that ignores the older browsers which still have a solid market share (when taken together) is that it’s not practical. I understand you weren’t intending to address the older browsers, but without mentioning anything about what to do for fallback, it leaves this article purely in the land of “cool, but i can’t use that much”, kinda like CSS3, at least for those of us who still have to support “all” browsers.
yes, JSON-P is a good approach, if the server you are targetting is capable of doing so. but it’s probably a more extensive change to retrofit an existing application (that doesn’t have a JSON-P API) with JSON-P than it is to be able to query for any kind of text, as the flXHR option can allow. yes, relying on flash sucks, but when all you have to do to a server is add one small little policy text file, that is attractive compared to the more complicated adjustments that this article suggests, or that JSON-P server API would entail.
the other problem with JSON-P is that (at least without libraries) it’s usually a very different interface from regular XHR, which makes code harder to fork on. for this reason specifically, I built jXHR, which (like flXHR) is an XHR API-compliant wrapper around JSON-P cross-domain Ajax. also, jXHR has some cool error handling that normal JSON-P approaches don’t tend to give you.
Kyle Simpson on May 26th, 2010 at 9:19 am
Wow, this is really happening!
Wonderful job.
memo on May 26th, 2010 at 9:32 pm
[...] C. Zakas concludes the above in his post on cross domain with cross origin resource sharing (and XDR for [...]
Ajaxian » COR Blimey! Cross domain Ajax is really here on May 27th, 2010 at 7:20 am
FireFox works well but I can’t get CORS to work in Chrome, especially with credentialed requests or responses which set cookies. I suspect Chrome is stricter in something but I can’t figure out what needs to be done.
Tarun Elankath on May 27th, 2010 at 11:25 am
Ok, apparently content scripts can’t make cross origin XHR’s in chrome. Setting all the Access Control headers doesn’t work. Apparently, it only works for extensions. Aaaargh!!
Tarun Elankath on May 27th, 2010 at 11:28 am
I sincerely apologize for the comment overload! But I figured a lot of people are going to google and hit this page and so I might as well add what I found out. [too lazy to make my own blog post]
CORS is more complicated than it seems. Please note that even in FireFox if you wish to access a credentialed resource, you cannot use wildcarding. i.e. this can’t be done
Access-Control-Allow-Origin: *
You need to reply back with the exact origin the request came from which is a big pain. I decided to do this in my java servlet:
if (origin != null) {
resp.setHeader("Access-Control-Allow-Origin", origin);
} else {
resp.setHeader("Access-Control-Allow-Origin", "*");
}
And it doesn’t even end there, you also need to set the withCredentials property of your xhr object to ‘true’.
if ("withCredentials" in xhr ) {
xhr.withCredentials = 'true';
}
FireFox will then give a challenge dialog where you can enter a username/password. And no – specifying a username/password to open doesn’t appear to work…
Still experimenting to figure out CORS in chrome…
Tarun Elankath on May 27th, 2010 at 11:38 am
Minor nitpick, but Origin doesn’t include path, so the trailing slash should be dropped:
Origin: http://example.com
Instead of:
Origin: http://example.com/
G Reimer on May 27th, 2010 at 12:04 pm
[...] the intrepid Nicholas Zakas has a super-handy introduction to browser-native cross-domain Ajax via Cross-Origin Resource Sharing (CORS). CORS is actually pretty well supported among modern browsers, but for whatever reason, it [...]
JavaScript Magazine Blog for JSMag » Blog Archive » News Roundup: Great Tutorial Resources, Akshell, and Web Audio Manipulation on May 28th, 2010 at 1:13 am
@Kyle – I think you’ve gotten a bit jaded.
Not everything needs to work in all browsers, that’s what progressive enhancement is all about!
@Tarun – thanks for the additional info. I didn’t delve too deeply into credentialed requests, very interesting stuff!
@G Reimer – thanks, fixed. Force of habit.
Nicholas C. Zakas on May 28th, 2010 at 1:44 am
@Nicholas — I understand there’s plenty of things that don’t have to work cross-browser… like rounded corners, lightbox effects, animations, shadows, geolocation, etc. All those are things that “enhance” the look and usability of a site or application, and so rightly, as you “progress” through from less to more capable browsers, you should get more “enhancement” as a result.
But I find it very hard to construct many use cases where cross-domain Ajax falls into that category. It seems to me that most of cross-domain Ajax usage would be a core functionality of a site/application, not just a feature enhancement. I’ve written dozens of web applications that flat out relied on cross-domain ajax — without it (in some form or fashion) they would not exist.
Consider mashups: if a google maps mashup didn’t have cross-domain Ajax of some sort, the mashup would be completely crippled and useless. I suppose some simple use cases for conditional cross-domain Ajax might be decorative widgets like twitter/rss feeds, etc.
Maybe I’m just missing it… in your mind, when writing this article, what non-critical “progressively enhanced” use cases were you considering for mainstream cross-domain Ajax?
Kyle Simpson on May 28th, 2010 at 6:55 am
does anyone have this working on iphone? i have it working in safari, chrome, ff, and android’s chrome, but iphone’s safari doesn’t work for me. any tricks on getting cors working for iphone?
colin on May 30th, 2010 at 3:36 pm
ps. the iphone *simulator* works, but the iphone doesn’t : /
colin on May 31st, 2010 at 11:20 am
[...] using CORS with some additional use cases described including credentialed requests check out this blog post from Nicholas Zakas.  Between his post and the CORS specification, you should be able to fill in the details you need [...]
Timothy Fisher’s Blog » Blog Archive » Cross-domain Ajax with CORS on June 1st, 2010 at 11:12 am
update:
I now have a reproducible test showing that iPhone’s MobileSafari can successfully send a POST request via JavaScript’s XMLHttpRequest, but only to a resource with the same origin as the host page. When the resource has a different origin (i.e., for cross-domain POST requests), no post data is sent to the server. The request’s Content-Length header has the value 0.
Here is the test showing cross-origin requests failing:
http://moock.org/iphonecors/posttest.html
Here is the test showing same-origin requests succeeding:
http://unionplatform.com/xmlhttptest/posttest.html
All info/comments much appreciated!
colin
colin on June 2nd, 2010 at 7:26 pm
[...] good article about CORS that summarizes the CORS [...]
Introducing Rack::CORS — Don't Forget to Plant It! on June 9th, 2010 at 8:17 am
It is needed to set the content-type for a POST with Google Chrome.
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
Do that only when using XMLHttpRequest. XDomainRequest does not allow to set headers.
Ivo Danihelka on June 19th, 2010 at 8:47 am
To clarify the need to set the content-type header:
Google Chrome XMLHttpRequest is using application/xml content-type by default.
That makes the Content-Type header to be non-”simple header”. Only the following content-types are defined to form a “simple header”: application/x-www-form-urlencoded, multipart/form-data, or text/plain.
The non-simple headers would need to be allowed by the server:
Access-Control-Allow-Headers: Content-Type
Ivo Danihelka on June 19th, 2010 at 9:17 am
It appears the “withCredentials” in xhr check does not work in Chrome’s incognito mode, although CORS appears to work just fine in that mode.
Brian Kuhn on June 24th, 2010 at 12:16 am
Forget my previous comment. The “withCredentials” in xhr check does work in Chrome incognito. It turns out that it only doesn’t work in Chrome incognito when Firebug Lite is on the page…
Brian Kuhn on June 24th, 2010 at 1:59 am
Hey There ! Is it possible to make https:// request from http:// using CORS ? so basically, cross protocol requests using CORS . I know cross-domain includes cross protocol, but the spec of CORS(ref: http://www.w3.org/TR/cors/#hosting-specification-security -6.3 Security) mentions user agent are independent to follow their policy and may prevent allowing https to http calls.
Mayank on July 1st, 2010 at 2:38 pm
[...] We require, however, POST requests (and actually also PUT or DELETE). However, JSONP is basically a hack that works only with GET. Some research into this issue revealed that there is actually one way round it, called Cross-Origin Resource Sharing. CORS was proposed by the W3C. An explanation with nice code examples can be found in this blog entry. [...]
Cross Domain AJAX with Restlet and jQuery « Semantic Thoughts on July 8th, 2010 at 4:42 am
I have tested CORS in Firefox 3.6, Chrome 5 and Safari 5and found that only Chrome can handle requests to servers with authentication properly. I tested cross-domain PROPFIND request with Basic, Digest and NTLM and found that Firefox supports only Digest authentication (it does not support Basic even with SSL for some reason) while Safari does not support any authentication for PROPFIND requests at all.
It is a great disappointment as PROPFIND and other WebDAV verbs are critical for our product, hope they will fix it.
We have published the results here: http://www.webdavsystem.com/ajaxfilebrowser/programming/cross_domain. See Cross-Domain Requests with Authentication section at the bottom of the page.
WebDAV on July 29th, 2010 at 11:24 am
[...] Cross-domain Ajax with Cross-Origin Resource Sharing | NCZOnline. This entry was posted in Ajax and tagged ajax, cors, cross-domain, XMLHttpRequest. Bookmark the [...]
Cross-domain Ajax with Cross-Origin Resource Sharing | Javascript on August 18th, 2010 at 4:29 am
[...] geht dann, wenn a) der Browser das bereits unterstützt, und b) die Gegenstelle auch mitspielt. Cross-domain Ajax with Cross-Origin Resource Sharing | NCZOnline __________________ RGB is totally confusing – I mean, at least #C0FFEE should be brown, [...]
jQuery (crossdomain) Ajax - php.de on August 30th, 2010 at 6:36 pm
There is one thing about CORS I can’t wrap my head around.
As far as simple methods are concerned, how is it any better than dropping the same-origin policy outright?
As I see it there’s nothing preventing an attacker from setting up their own server to host, say, an XSS payload, and then adding the
Access-Control-Allow-Originheader to its responses, either with a wildcard or exclusive value; what then would prevent an exploited XHR client from happily swallowing whatever the server throws at it?It makes a little more sense as a mechanism through which the server can selectively allow external access while protecting itself against unwanted / malicious requests, but then again a look at the
Refererheader would be just as effective.I’m happy to find a way for enabling cross-domain XHR’s in my web apps, but I fail to see the security in it. Why not simply add some sort of whitelist facility to the XMLHttpRequest object, so client programmers could enumerate the domains they deemed acceptable for access?
Helio on October 15th, 2010 at 3:39 pm
I agree with Helio. CORS does not address the problem at hand; that is to say that the data acquired via a cross-domain AJAX request may pose a security risk to the server making the request. CORS only deals with the handshake of the server that will be processing this request and sending data back, it has nothing to do with whether or not this data is malicious. As I see it, all this does is get rid of the same-origin policy, making cross-domain AJAX possible without hacky solutions, but with the same vulnerabilities it has always had.
That is not to say that CORS is a bad idea. It seems to me that it makes requests to the server itself more secure, but it just doesn’t really have any merit on cross-domain AJAX security. I think the only solution to the cross-domain AJAX request debate is for the user agent to implement parsing and/or validation for known data types within XHR complete with error handling (much like what jQuery.ajax does with the dataType set to “json”).
Kyle Florence on November 29th, 2010 at 9:20 pm
[...] 92. Cross-domain Ajax with Cross-Origin Resource Sharing [...]
RegexHacks :: Blog » The Top 150 Web Development Highlights from 2010 on December 31st, 2010 at 4:43 am
Comments are automatically closed after 14 days.