Monday, September 24, 2012

Cross-domain AJAX with JSONP



Anyone who develops Javascript long enough undoubtedly runs into difficulties involving the various security features all browser vendors implement. These security features are a good thing -- they protect us from malicious users hijacking our browsing experience. But they can certainly cause some headaches. The security feature that presents the most difficulty for us as developers is the same origin policy.
In a nutshell, this policy prevents pages from two different domains from modifying each others properties, using XMLHttpRequest, setting cookies etc. For instance, Example.com and OtherExample.com can't get references to each others document properties and can't set cookies on each other. Additionally, Example.com can't use XMLHttpRequest (aka AJAX) to load a resource from OtherExample.com. This last bit is probably the biggest issue for developers today -- in todays world of open web services and mashups. How do you consume a web service with Javascript if you can't load the data properly?

Solution 1: Use a server-side proxy

The first way around this problem is to use a very simple server-side script that acts as a proxy to the web service. So instead of requesting AJAX from the remote site, you request it locally on your own domain through the proxy. The proxy itself has it's programming logic to send your request off to the remote site, gather the data, and then serve it back to you.
.________________________.
| Client on YourSite.com |
'-\/---------/\----------'
  ||         ||
  ||_________||__.
  | AJAX Request |
  '---\/-----/\--'
      ||     ||
      ||_____||______________.
      | YourSite.com         |
      | (Ex. ajax_proxy.php) |
      '---\/-----/\----------'
          ||     ||
          ||_____||__________.
          | OtherSite.com    |
          '------------------'
Since your server-side script has no qualms about fetching data from another domain, you can successfully proxy all AJAX like this without running into any trouble. And then since the client browser is fetching the data from the local domain (even though you're making another request behind the scenes), it doesn't violate the same origin policy.
An example proxy script might look something like this (maybe a little more complex if you're handling POST data too):
  1. $url = 'http://othersite.com/someservice?' . http_build_query($_GET);
Remember, the sole purpose of the proxy is just so the client browser can load the data locally on the same domain so the same origin policy isn't violated.

Drawbacks

This method works well but has two obvious drawbacks. First is of course that you need a server-side script at all. Especially if you are providing a service, this raises the barrier for entry and makes it harder for "noobs" to use your widget -- it's not a simple "paste this code into your footer" instruction; you also need to explain how to install the proxy service.
The second drawback is that your own servers are making these requests which makes the whole process slower, but also eats up your resources -- both your processing power and even just simple bandwidth.

Solution 2: JSONP

The second solution is to use JSONP -- "JSON with Padding." This is a technique that lets you get around the same origin policy. In order to use JSONP, the service you are requesting needs to support it.
So in the last solution, the consumer (the user using the service) required an ajax_proxy.php or whatever server-side proxy script. In this solution, the provider (Digg, Amazon, Yahoo -- whatever) needs to support JSONP themselves. Thankfully, these days the idea is catching on and it's likely JSONP is an option. And if you're a service provider, then you'll want to build it in to your service for sure.

How it works

A normal JSON request is sent using XMLHttpRequest and the reply looks something like this:
Plain TextJAVASCRIPT:
  1. {'uid'23'username''Chroder''name''Christopher Nadeau'}

Now the truth is, JSONP isn't AJAX at all technically speaking because it does not use XMLHttpRequest. JSONP requests are made by dynamically inserting a <script> tag into the DOM. For example, you'd insert this:
  1. <script type="text/javascript" src="http://othersite.com/service?all=your&params=as&usual=gohere"></script>

Then that remote Javascript file is loaded and contains an actual Javascript function call. For example:
Plain TextJAVASCRIPT:
  1. handleJsonReply({'uid'23'username''Chroder''name''Christopher Nadeau'});

In other words, instead of reading AJAX data directly into a variable in Javascript, you define a callback function that is called when the data arrives, and the first parameter is the data itself as an object literal. If you were a provider implementing JSON on your service you might have something like:
  1. // A service provider accepts a 'callback' parameter
  2. // that it uses to wrap an AJAX reply
  3.  
  4. // Imagine we did some work here
  5. $json = json_encode($mydata);
  6.  
  7. // Using JSONP
  8. if ($_GET['callback']) {
  9.     echo $_GET['callback'] . "($json);"// somefunction({data here});
  10.    
  11. // Normal JSON
  12. } else {
  13.     echo $json;
  14. }

The Javascript source code result of that request is nothing but a real executable function call with a Javascript object literal. It's as if you added the tag yourself and it called some functions -- the difference being that the JS is written by the producer on-the-fly and embeds the data you want right in the code. This is why it all works; it's not actually AJAX at all, it's just loading a remote Javascript file that happens to have some useful data in it.
Using a library like jQuery that supports JSONP, these details of inserting the special script tag and creating the special callback function are all taken care of automatically. Using a JS library, usually the only difference between JSONP and real AJAX is that you enable a 'jsonp' option.

Drawbacks

The first limitation to this method is that you have to rely on the provider to implement JSONP. The provider needs to actually support JSONP -- they need to wrap their JSON data with that callback function name.
Then the next limitation -- and this is a big one -- is that JSONP doesn't support POST requests. Since all data is passed in the query string as GET data, you are severely limited if your services require the passing of long data (for example, forum posts or comments or articles). But for the majority of consumer services that fetch more data than they push, this isn't such a big problem.

JSONP with jQuery

Step 1: Make sure the provider supports JSONP.
Step 2: Set the dataType option to jsonp, and if the provider uses a different GET param other than 'callback', specify the jsonp option to that parameter name.
Plain TextJAVASCRIPT:
  1. $.ajax({
  2.     // ... Use the AJAX utility as you normally would
  3.     dataType: 'jsonp',
  4.     // ...
  5. });

jQuery will generate a unique callback name for this request (something like json1268267816). Thus, the reply from a web service would be something like:
Plain TextJAVASCRIPT:
  1. json1268267816({'uid'23'username''Chroder''name''Christopher Nadeau'});
But jQuery handles it all seamlessly, so you as the developer just handle it like a normal AJAX request using the same jQuery success/failure/complete callback hooks.

2 comments: