As more application code moves from the server to the client, it's increasingly common to use JavaScript to load JSON data that lives on a different domain. Traditionally, this cross-domain (or more accurately, cross-origin) querying is blocked by browser security, but there is a popular technique for working around this limit called JSONP (or JSON with Padding).

With JSONP, a server returns JSON data wrapped in JavaScript, causing the response to be evaluated by the JavaScript interpreter instead of being parsed by the JSON parser. This technique takes advantage of a browser's ability to load and execute scripts from different domains, something the XmlHttpRequest object (and, in turn, Ajax) is unable to do. It opens the door that enables JavaScript applications to load data from any remote source that supports JSONP directly from the browser.

OData and JSONP

OData, an up and coming RESTful data service schema that is trying to "standardize" the REST vocabulary, can support JSONP when the backing OData implementation supports it. In its most basic form, OData is simply AtomPub XML or JSON that conforms to OData's defined query keywords and response format. How you generate that XML or JSON- and what features you support (like JSONP)- is up to you.

A very popular approach for building OData services (given OData's Microsoft roots), one used by Netflix, is to implement an OData end-point using Microsoft's ADO.NET Data Services. With this implementation, most of the OData heavy-lifting is done by a framework, and most of the common OData schema features are fully supported.

Critically missing with vanilla ADO.NET Data Services, though, is support for JSONP.

To add support for JSONP to ADO.NET Data Service OData implementations, a behavior add-on is needed. Once installed, the new behavior adds two OData keywords to the RESTful query vocabulary:

  • $format - which allows explicit control over the response format from the URL (i.e. $format=json)
  • $callback - which instructs the server to wrap JSON results in a JavaScript function to support JSONP

With this support, OData services on any remote domain can be directly queried by JavaScript.

Querying Netflix Using jQuery

Let's put our new found JSONP support to the test. First we need a RESTful query:

http://odata.netflix.com/v2/Catalog/Genres

By default, this query will return all Netflix movie genres in XML AtomPub format. Let's make the results more JavaScript friendly by "forcing" a JSON return type (technically not needed in our jQuery code, but it makes debugging easier):

http://odata.netflix.com/v2/Catalog/Genres?$format=json

That's better. Now, how would we use this with jQuery to get some data? Like this:

$.ajax({
    url: "http://odata.netflix.com/v2/Catalog/Genres?$format=json",
    contentType: 'application/json; charset=utf-8',
    type: 'GET',
    dataType: 'jsonp',
    error: function (xhr, status) {
        alert(status);
    },
    success: function (result) {
        console.log(result);
        //TODO: Display the result
    }
});

What's going on in this jQuery snippet:

  • We're using jQuery's $.ajax API to query our OData endpoint
  • We're setting the request contentType header to 'application/json' (which can auto-trigger OData JSON responses)
  • We're telling jQuery that this is a JSONP request with the dataType property
  • And finally, we're handling the Success and Failure events

Upon running this snippet, though, you may encounter this dreaded JavaScript error, accompanied by a "parsererror" alert:

Uncaught SyntaxError: Unexpected token :

What? Looking at your network traffic, you see the request to Netflix. You see the response with JSON. The JSON data looks good. Why isn't jQuery parsing the JSON correctly?

When you tell jQuery that the dataType is JSONP, it expects the JSON results to be returned wrapped in JavaScript padding. If that padding is missing, this error will occur. The server needs to wrap the JSON data in a JavaScript callback function for jQuery to properly handle the JSONP response. Assuming the OData service you're using has added the proper support, that means we need to modify our RESTful URL query one more time:

http://odata.netflix.com/v2/Catalog/Genres?$format=json&$callback=?

By adding the "$callback" keyword, the OData endpoint is instructed to wrap the JSON results in a JavaScript function (in this case, using a name auto-generated by jQuery). Now our data will be returned and properly parsed.

Using the Kendo UI Data Source

The Kendo UI Data Source is a powerful JavaScript abstraction for binding to many types of local and remote data. Among the supported remote data endpoints is OData. Since the Kendo UI Data Source knows how OData should work, it can further abstract the raw jQuery APIs and properly configure our query URL.

In this example, we can configure a Kendo UI Data Source with a basic OData query, like this:

var data = new kendo.data.DataSource({
    type: "odata",
    transport: {
        read: "http://odata.netflix.com/v2/Catalog/Genres"
    }
});

Notice that our URL does not include any of the OData keywords, like $format or $callback. We told the Kendo UI Data Source that this is an OData endpoint by setting the "type" property to "odata," and with this simple configuration, Kendo UI handles the rest. When the data source is used, a request is made to the following RESTful URL:

http://odata.netflix.com/v2/Catalog/Genres?$format=json&$inlinecount=allpages&$callback=callback

As you can see, Kendo UI has automatically added the needed parameters. In fact, Kendo UI can do much more. If we configure the Kendo UI data source to use server paging and filtering, it will automatically build the proper OData URL and push the data shaping to the server. For example, let's only get Genres that start with "A", and let's page our data. Kendo UI configuration is simply:

var data = new kendo.data.DataSource({
    type: "odata",
    serverPaging: true,
    serverFiltering: true,
    pageSize: 10,
    filter: [{
        field:"Name",
        operator:"startswith",
        value:"A"
    }],
    transport: {
        read: "http://odata.netflix.com/v2/Catalog/Genres"
    }
});

A quick configuration change, and now we will precisely fetch the needed data from the OData service using this Kendo UI generated URL:

http://odata.netflix.com/v2/Catalog/Genres?$format=json&$inlinecount=allpages&$callback=callback&$skip=0&$top=10&$filter=startswith(Name,'A')

We've gone from very raw, low-level jQuery $.ajax queries, where we had to remember to set the correct content type, request type, data type, and manually construct our query with the needed parameters, to a nicely abstracted JavaScript data source that handles much of the dirty work for us.

What about CORS?

CORS, or Cross-Origin Resource Sharing, is a newer pattern for accessing data across domains using JavaScript. It aims to reduce the need for JSONP-style hacks by providing a native browser construct for using normal, XHR requests to fetch data across domains. Unlike JSONP, which only supports GET requests, CORS offers JavaScript developers the ability to use GET, POST, and other HTTP verbs for a more powerful client-side experience.

Why, then, is JSONP still so popular?

As you might expect, CORS is not as fully supported as JSONP across all browsers, and more importantly, it requires servers to include special headers in responses that indicate to browsers the resource can be accessed in a cross-domain way. Specifically, for a resource to be available for cross-domain (note: I'm saying domain, but I mean "origin" - domain is just a more familiar concept), the server must include this header in the response:

Access-Control-Allow-Origin: *

If this header is present, then a CORS-enabled browser will allow the cross-domain XHR response (except IE, which uses a custom XDomainRequest object instead of reusing XHR for CORS…of course). If the header value is missing, the response will not be allowed. The header can more narrowly grant permissions to specific domains, but the "*" origin is "wide open" access. Mozilla has good documentation on detecting and using CORS, and the "Enable CORS" website has info on configuring server headers for a number of platforms.

Using CORS with jQuery and OData

The primary obstacle with CORS is the server. Very few public web services that provide JSON data today have the necessary CORS response headers. If you have a service with the headers, everything else is easy! In fact, you don't really have to change your JavaScript code at all (compared to traditional XHR requests). Assuming I have a CORS-enabled OData service, I would use code like this to query with jQuery:

$.ajax({
    url: "http://localhost/NorthwindSvc.svc/Orders?$top=10",
    contentType: 'application/json; charset=utf-8',
    type: 'GET',
    dataType: 'json',
    error: function (xhr, status) {
        alert(status);
    },
    success: function (result) {
        console.log(result);
        //TODO: Something with the CORS result
    }
});

As you can see in this snippet, CORS is completely transparent and supported by jQuery. If we're migrating form JSONP, then we change the "dataType" from "jsonp" to "json" and we remove the JSONP callback parameter from our OData query. jQuery even navigates the different XDomainRequest object in Internet Explorer!

I expect CORS will become much more popular in the next 12 to 18 months. We'll explore ways the Kendo UI Data Source can embrace and enrich this pattern, too.

----

If you're going to start building JavaScript applications, you must get comfortable querying data from the browser. I've demonstrated a few ways you can use jQuery and Kendo UI to query OData end-points, but many of the concepts translate to other service end points. I hope this helps enrich your understanding of cross-origin data access and helps you build better HTML5 and JavaScript apps!


About the Author

Todd Anglin

is an avid HTML5, CSS3, and JavaScript advocate, and geek about all things web development. He is an active speaker and author, helping developers around the world learn and adopt HTML5. Todd works for Telerik as VP of HTML5 Web & Mobile Tools, where his current technical focus is on Kendo UI. Todd is @toddanglin on Twitter

Related Posts

Comments