Thursday 21 February 2013

A JSONP and CORS odissey

I guess we should all have a basic idea of what the Same Origin Policy restriction and JSONP are. Let's say we have an application hosted on application.org, and it tries to gather data via ajax from dataprovider.org. We're trying to fetch data from a different domain, so the Same Origin Policy comes into play and the Ajax call won't work. In order to workaround this problem (yes, it's a security feature, but it's also a limitation), some clever guy came up with JSONP. In short, instead of doing an Ajax call to the "foreign" domain, you request a script from it by adding on the fly a <script> element to your document with its source pointing to the "foreign" domain. This domain will return a piece of javascript code, with the expected data wrapped in a function call. That function call should be a function already defined in your code, a function that will receive as parameter the data that you expect from the server and will deal with them (like you do with your typical Ajax success,error callbacks). The idea is pretty cool, but has its limitations:

  • it only works for GET requests, if your original Ajax call was posting a complex JSON object in the Request body, you're hopeless, cause you would need to serialize it some way into the url, and the classical Form encoded data won't be enough.
  • your server side code needs to be adapted to wrap the response data into the javascript function call (which name you should also have passed over from the client).

I'm doing some "research/prototyping/head scratching/hair-loss promoting" experiments at work for a new project, and it involves some JSONP calls. With the current set up, both servers are running on the localhost listening on different ports. I had read that different ports already break the Same Origin Policy, but just as a matter of curiosity I wanted to test it myself and see the normal Ajax call fail. Well, to my complete surprise, the call seemed to be making it to the server (my server side debugger was being hit):

and indeed Firebug was showing a 200 OK response, but somehow the jQuery Ajax error callback was being invoked, with the jqXHR object with status set to 0. Well, I was feeling quite bewildered, to my understanding, the browser should be raising an error before calling into the server, just when realizing that the Ajax call was being addressed to a different domain (port in this case), and indeed, that's also what Wikipedia expects
The W3C recommends that browsers should raise an error and not allow the request of a URL with either a different port or ihost URI component from the current document
So, WTF?

Well, it's here where CORS seems to pop its head and combined with the development server that I'm using (IIS Express) make this mess. This document in MDN appears to explain CORS quite well.
So, my understanding (part of it is mainly a guess) is that modern browsers (like Firefox, that is what I loyally use for all my development) seem not to automatically raise an error when trying to do a cross domain ajax request. They do the request, adding the Origin header to it, and see whether the Server accepts the response. I guess that a proper production server should see this Origin header and if it doesn't agree with the request, just respond with an error, and if on the contrary, it agrees with it, should do the normal processing, and then add a Access-Control-Allow-Origin header to the response (so that the browser can be sure that the server was OK with this call. In my case, I guess IIS Express is just letting the request go through and sending a response as if it had been a call from the same domain (most likely IIS Express is not CORS aware). So, though the call was correctly processed, Firefox can't find the response header and thus it decides to throw an error.

Something that I've also found slightly confusing is the jQuery JSONP magic and its documentation. Long in short, if you're using $.ajax, all you should need to make your same domain calls work when addressing a different domain is setting the dataType property to "jsonp" instead of "json" (and as explained above, bear in mind that it will only work for GET requests, and therefore the data property will not work). For the most part, you can forget about the jsonp and jsonpCallback properties that are also available in the options object.
Having a basic understanding of JSONP this seems both magical and confusing (indeed I thought I was missing something). I was wondering if I didn't need to declare my JSONP callback function on the client side, and furthermore the success-error ajax callbacks seemed confusing to me, after all this was not a real ajax call...
Well, jQuery will take care not only of creating the script element for you, but also of creating a callback function on the client side that will invoke the success-error callbacks accordingly, and then will add it to the querystring to let your server know. So, where the documentation says:

Adds an extra "?callback=?" to the end of your URL to specify the callback

it means that the url for the script's src will be something like this:

http://localhost:55579/rpc/Geographic/GetCitiesJsonp?callback=jQuery1710571054381039827_1361460447827&_=1361460450663

and your server should be returning something like this:

jQuery1710571054381039827_1361460447827(["Berlin","Leipzig","Dresden"]);

OK, I have to say that I don't fully agree with using Ajax semantics for something that is not Ajax, I would have preferred a separate $.jsonp method.

No comments:

Post a Comment