Getting CORS Working

Recently I ran a workshop where I ran a small section of the workshop on CORS and how to enable it. In the past, I’ve found it to be very easy but this time around everything backfired and it didn’t work. So after the workshop I went about understanding why the CORS demo it didn’t work, and how to get it working.

Disclaimer: other people have explained this before, this post is mostly for me!

CORS?

Cross Origin Resource Sharing – i.e. cross domain Ajax. The technical side of getting CORS to work has been explained in a lot more detail by Nicholas C. Zakas in his article Cross-domain Ajax with Cross-Origin Resource Sharing, (i.e. IE8, for reasons beyond most, use XDomainRequest – utterly bespoke – but that’s Microsoft for you).

Simple CORS

CORS, if you’re not doing anything clever is easy. The client side should just be the following code (assuming we’re not IE – see link above for IE hoop jumping):

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://different-domain.com');
xhr.onreadystatechange = function () {
  if (this.status == 200 && this.readyState == 4) {
    console.log('response: ' + this.responseText);
  }
};
xhr.send();

So just a simple XHR send – in fact, exactly the same with the exception that the url goes to a domain that’s different to the origin.

The only thing you need to have on your different-domain.com server is an additional header that tells the browser it’s okay to go cross domain. In PHP that header looks like this:

<?php
header('Access-Control-Allow-Origin: http://www.some-site.com');
?>

Equally, if you want to make the API public to anyone to access, you can use:

<?php
header('Access-Control-Allow-Origin: *');
?>

As simple live example of this can be seen here: jsbin.com/oxiyi4 which makes a request to remysharp.com/demo/cors.php which includes a rule that allows any origin to access the resource.

This is simple and easy. However, it’s the preflight that causes confusion. That’s where it all went wrong for me.

Preflight

In plain Remy language preflight is an additional request the XHR object makes to make sure it’s allowed to actually make the request.

By default, there’s no preflight, so why was this a problem for me?

Setting custom headers on XHR requests triggers a preflight request.

Detecting Ajax on the server

Most JavaScript libraries send a custom header in the XHR request which can be sniffed on the server side to allow us to simple detect an Ajax request:

<?php
$ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 
        $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';

if ($ajax) {
  // handle specific Ajax differently...
}
?>

This way my server side code handles regular traffic differently to Ajax traffic.

When I manually set the x-requested-with header on the XHR object, it triggered the preflight, which is where it all hit the fan.

Handling the preflight x-requested-with

The request process, with a preflight, if successful should look like the follow request exchange (note that I’ve stripped some headers that weren’t pertinent to this article, like User-Agent, etc).

This is a real request from one domain to place an XHR request for http://jsbin.com/canvas/73/source.

Client sends XHR request with custom header:

OPTIONS /canvas/73/source HTTP/1.1
Host: jsbin.com
Access-Control-Request-Method: GET
Origin: http://jsconsole.com
Access-Control-Request-Headers: x-requested-with

Server responds to OPTIONS request (no content served in this case):

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: X-Requested-With

Client sends GET request as it has permission to do so:

GET /canvas/73/source HTTP/1.1
Host: jsbin.com
x-requested-with: XMLHttpRequest

Server responds to GET request with content:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Length: 977

This only works, because my server side is specifically looking out for the OPTIONS request, and handling it as you’ll see in my following server code.

Server code to handle prelight

The following PHP code simply checks for the OPTIONS request method. If OPTIONS has been used to make the request, and the user is requesting using CORS, my server responds saying that the X-Requested-With header is permitted:

// respond to preflights
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
  // return only the headers and not the content
  // only allow CORS if we're doing a GET - i.e. no saving for now.
  if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'GET') {
    header('Access-Control-Allow-Origin: *');
    header('Access-Control-Allow-Headers: X-Requested-With');
  }
  exit;
}

Now the XHR CORS request allows the X-Requested-With header, the rest of my code remain in place, and the flag to indicate it’s an Ajax request if the X-Requested-Header is present works as it did before.

Avoid preflight if possible

Jumping through these hoops was pretty tricky, and only after I solved this puzzle did I breakout Wireshark for packet sniffing – which might have helped to debug the whole issue in the first place.

Funnily enough, when making a CORS request using jQuery, the JavaScript library specifically avoids setting the custom header, along with a word of warning to developers:

// For cross-domain requests, seeing as conditions for a preflight are akin to a jigsaw puzzle, we simply never set it to be sure.

So possibly the best advice, if possible, is to avoid setting the custom header if you don’t want to do the preflight dance. Otherwise good luck my friend, I hope this has helped!

  • JulienW

    As for jQuery, the check to add X-Requested-With or not is new to the very recent 1.5.2.

    cf http://bugs.jquery.com/ticket/8423

  • http://qfox.nl Peter van der Zee

    Yeah CORS can be tricky. Especially as when browsers throw random cryptic messages, if any at all, when something goes wrong. The network tabs all do different stuff, some show the preflight, others don’t. This is especially problematic when things go bad and you have no idea whether it’s a client or server issue.

    I believe I had set it up for Chrome, but then firefox demanded a different header as well. Then later IE demanded another header as well. It’s not easy being blue…

    But when it works… :p

  • http://hanblog.info Rik

    Also, not triggering a preflight saves you a roundtrip.

  • Drew

    I had the exact same headscratching experience.

    Note that, for awhile, (haven’t seen if it was fixed) Chrome’s net panel/dev tools weren’t even telling me that it was making an OPTIONS request that was failing: it simply threw an X-domain error without explanation, and I was baffled until checking Firebug. Once I set the right header on OPTION, all was well again. And then jQuery fixed the bug causing the unnecessary preflight, and all was _really_ well.

    Of course, CORS is just weird in general. It really does nothing to fix the security issues that make cross-domain requests so dangerous: a server that wants to send malicious code will, of course, WANT to allow people to access it cross-domain.

  • Pingback: JavaScript Magazine Blog for JSMag » Blog Archive » News roundup: Modern JavaScript, V8Monkey and SpiderNode, Adapt.js

  • http://itcutives.com Jatin

    I am a PHP developer since many months (10+), but haven’t worked on any cross domain Ajax. You article came in time, I was thinking of learning it. Thanks

  • Dino

    I think your PHP code implementing the preflight response is wrong. It does not check for the Origin header. In http://www.w3.org/TR/cors/ , it says:

    If [in the preflight request] the Origin header is not present terminate this set of steps. The request is outside the scope of this specification.

    According to my reading of that statement, your resource server should verify that the Origin header is present in the request and non-empty before responding with anything that includes Access-Control-Xxx-Xxxx .

    See sec 6.2 of that document, “Preflight Request”.

  • Cristian Rusu

    Hello

    I have a problem making this work:
    I can do a post just fine and it’s processed, but no response is returned.

    What kind of response should I make for a post in order to receive it in browser, right now I get no response body other than headers transfer encoding chunked

    Thanks

  • David Vonka

    Thank you very much. I now have code that POSTs data to another domain both in IE8+ and real browsers. Let me give you my js samples and server side Java, hopefully it helps someone. The core js is plain js, for unimportant code I use jquery:

    
    function submitText(text){
    if (jQuery.browser.msie){
       var xdr = new XDomainRequest();
       xdr.open('POST', 'http://otherdomain.semantacorp.com:8080/plugins/inexutils/createpage.action');
       xdr.onload = function () {
             jQuery('#lubo-test').html(this.responseText);
       };
       xdr.send("parent=4587526&template=ask&labels=setmeta-state-open,question&fromPage=4587526&reltype=created-question&content="+escape(text)+"&title=toto+je+kratkej+text&meta_objecttype=question");
    } else {
       var xhr = new XMLHttpRequest();
       xhr.open('POST', 'http://otherdomain.semantacorp.com:8080/plugins/inexutils/createpage.action');
          xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
      xhr.onreadystatechange = function () {
        if (this.status == 200 && this.readyState == 4) {
           jQuery('#lubo-test').html(this.responseText);
        }
      };
      xhr.send("parent=4587526&template=ask&labels=setmeta-state-open,question&fromPage=4587526&reltype=created-question&content="+escape(text)+"&title=toto+je+kratkej+text&meta_objecttype=question");
    }
    }
    
    jQuery(document).ready(function(){
      jQuery("#xxxx").click(function(){
          var text = jQuery('textarea[name=text]').val();
          submitText(text);
      });
    });
    
    

    Now, the server-side is Java within Confluence, but I guess you get the gist:

    response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
            response.setHeader("Access-Control-Max-Age","1000");
            response.setHeader("Access-Control-Allow-Headers","Content-type");
    .... the following code is needed for IE8 ...
    String text = Tools.convertStreamToString(request.getInputStream());
    ... and now parse the text var, which is like labels=blabla&fromPage=blabla....
    .. for other browsers this would work: ...
    String _parent = request.getParameter("parent");
    
    
  • Yiannis

    I am very confused with the XMLHttpRequest and the XDomainRequest reincarnation and would like some help. So here are my findings:

    The XDomainRequest in IE8 and IE9 seems to be some kind of XMLHttpRequest sub class(?)
    The XDomainRequest lacks the “withCredentials”

    Also, it submits data as plain/text and not as form forcing you parse you inputstream at the back end.
    Even if the CORS server “Allow-Headers” directive allows for the Set-Cookie to be read by the client, the XDomainRequest does not expose it making impossible to use cookie stored session iDs to be used for authentication.
    Finally if I am not wrong, it allows only POST and GET http methods rendering it useless for RestFull web services.
    This list is by no means complete and as I said it is based on my findings. However, here is where the confusion starts. I have an application where via Ajax I must:

    Obtain (cross domain) via GET an encryption key along with a session id associated with it.
    Encrypt my user password using this key (no problem here)
    Login to the cross domain (where I got the key at step 1) using the POST and x-www-form-urlencoded username and the encrypted password.
    Now for all the above reasons I cannot do this with the XDomainRequest:

    First because the XDomainRequest:open(method, url) sends only plain text and my third party application is expecting form (I can write a filter/request interceptor but this is not the point).
    Because my session id that arrives with the encryption key (step 1) is never sent back to the cross domain when login as a header since the XDomainRequest does not expose headers.
    Nevertheless if in IE8 and IE9 I instantiate a XMLHttpRequest disregarding, all is working fine!!! OK I do not get the onload event and I am not sure what is the story with the “withcredentials” but IE8 and IE9 seems to have no problem using the XMLHttpRequest for cross domain. But why? Aren’t all these contradictory? I am just trying to make some sense of this issue as I am afraid that using the XMLHttpRequest in IE8 and IE9 may come back and bite at some point.

    So unless if I am mistaken, either the XDomainRequest is practically useless or I somehow managed to bypass the whole CORS notion on IE9 and IE8.

    Any suggestion will be greatly appreciated Yiannis

  • https://twitter.com/aronwoost Aron

    Since this is a popular post, let me just add the following.

    When you use the current version of jQuery no X-Requested-With is added. However if you still get the OPTIONS preflight it might has to do with the fact that “Content-Type:application/json” is also interpreted as custom header. “Content-Type:application/json” is set by default in Backbone (and probably other modern libs).

  • Mark Evans

    Struggling with this issue. I want to avoid doing a preflight, if at all possible. Is any manually added header considered “custom”? I’m adding an Authorization header to use “basic auth”. Will this mean jQuery will always generate a preflight request automatically?

  • https://plus.google.com/+adityamenon aditya menon

    Thanks so much! I had actually written code to handle the preflight hairiness, but when $http wasn’t sending any preflight I was dumbfounded. Adding the X-Requested-With header fixed that.