facebook google plus twitter
Webucator's Free Ajax Tutorial

Lesson: CORS/JSONP

Welcome to our free Ajax tutorial. This tutorial is based on Webucator's Ajax Training course.

Browsers on our desktops and mobile devices implement the same-origin policy, preventing remote scripts to run if they come from external sites. We look here at two strategies for accessing remote data via Ajax: Cross-Origin Resource Sharing (CORS) and JSON with Padding (JSON-P).

Lesson Goals

  • Learn how and why browsers blocks scripts from external sites.
  • Learn how to use Cross-Origin Resource Sharing (CORS) to access remote data.
  • Learn how to use JSON with Padding (JSON-P) to access remote data.

CORS/JSONP: Accessing Remote Sites

Most of the time, accessing scripts from one domain to another - for instance, from example1.com to example2.com - isn't permitted because the same-origin policy allows scripts to run only if they match in protocol (http vs. https, for example), hostname (example1.com vs. example2.com), and post (port 80 or 443 for http or https traffic, by default). For obvious reasons, browsers enforce these rules to block potentially malicious exploits: it would be a poor sort of Internet on which clicking any link opened up threats from other sites.

Specifically, it is the response from the foreign-site script that our browser prevents us from consuming: Ajax requests to an external site are sent along, but - in the absence of some other mechanism - the response isn't accepted by our browsers.

Of course, there are times when we want to explicitly allow remote access, either sharing our own resources with external sites or purposefully leveraging resources available on foreign sites for our own purposes. In this lesson, we look at two strategies for accessing foreign-site resources: Cross-Origin Resource Sharing (CORS) and JSON with Padding (JSONP).

CORS

Cross-Origin Resource Sharing (CORS) is a mechanism for requesting fonts, scripts, and other resources from an origin (defined, as above, as the combination of domain, protocol, and port) other than the requesting origin. For instance, if sending a request from http://www.example.com, any of the following would be "cross origin":

  • http://mail.example.com (domain differs),
  • https://www.example.com (protocol differs),
  • http://www.example.com:8080 (port differs),

and, thus, scripts (or font, or other similar resources) would be blocked from these "foreign" sites. CORS offers a way for two sites to allow safe sharing of resources.

How CORS Works

CORS defines the communication between browser and server: specific headers in the HTTP request and HTTP response tell the browser that its OK to accept the resource. At its most basic, a server issuing an HTTP response which includes the header

Access-Control-Allow-Origin: *

is allowing access from all requesting domains. A more-restrictive response, for example

Access-Control-Allow-Origin: http://www.example.com

would allow access only from a particular domain.

The great thing for us, as web developers, is that CORS-enabled responses work just like responses from our own (same-origin) site: our code can process the JSON, XML, or other response we receive just as if we were making a request of a page or resource on our own server.

The CORS request/response cycle can get significantly more complex, with "preflight" requests sent by the browser and responded to from the server, before another set of request/response; the passing of cookies or other authentication mechanisms; and other sharing of data. Check out https://www.html5rocks.com/en/tutorials/cors/, an excellent tutorial on HTML5 Rocks, to delve deeper into the topic.

Check out http://enable-cors.org/resources.html#apis for a list of sites that offer CORS-enabled resources.

Let's look at an example, accessing a CORS-enabled site and a not-CORS-enabled site. Open CORSJSONP/Demos/cors-html5-rocks.html in your browser and in a code editor to review the code. No need to start the Node.js server for this example.

Code Sample:

CORSJSONP/Demos/cors-html5-rocks.html
<!DOCTYPE HTML>
<html>
<head>
---- C O D E   O M I T T E D ----

<script type="text/javascript" src="jquery-1.11.1.min.js"></script>
<script type="text/javascript">
	$(document).ready(function() {
		$('#btnhtml5rocks').click(function() {
			$('#ResponseContent h2').html('Response Content');
			$('#ResponseContent div').html('');
			$.ajax({
				type: 'GET',
				url: 'https://www.html5rocks.com/en/tutorials/file/xhr2/',
				success: function(response) {
					var article = $(response).find('article').first().html();
					$('#ResponseContent h2').html('Response Content - from HTML5Rocks');
					$('#ResponseContent div').html(article);
				}
			}).fail(function() {
				alert("failed!");
			});;
		});
		$('#btnnytimes').click(function() {
			$('#ResponseContent h2').html('Response Content');
			$('#ResponseContent div').html('');
			$.ajax({
				type: 'GET',
				url: 'http://www.nytimes.com/',
				success: function(response) {
					var article = $(response).find('article').first().html();
					$('#ResponseContent h2').html('Response Content - from NY Times');
					$('#ResponseContent div').html(article);
				}
			}).fail(function() {
				alert("failed!");
			});
		});
	});
</script>
</head>
<body>
	<button id="btnhtml5rocks">Fetch HTML5 Rocks</button>
	<button id="btnnytimes">Fetch NY Times</button>
	<br>
	<div id="ResponseContent">
		<h2>Response Content</h2>
		<div></div>
	</div>
</body>
</html>

We include jQuery via a script tag, and use jQuery to effect our Ajax calls.

The page presents two buttons, with ids btnhtml5rocks and btnnytimes, respectively, and use jQuery to listen for a click on each button.

When either button is clicked, we make an Ajax call to a remote site, setting the h2 tag's content to show from which source we receive the external data and setting the contents of #ResponseContent div, if successful in our Ajax call, to the contents of the first article tag from our received response.

Clicking the "Fetch HTML5 Rocks" button generates an Ajax call (via jQuery's $.ajax method) to https://www.html5rocks.com/en/tutorials/file/xhr2/. We set local JavaScript variable article to the contents of the first article found in the returned response and display the contents on our page.

Despite our making a call to a cross-origin (i.e. non-local) site, our code works. Specifically, it is the presence of the Access-Control-Allow-Origin: * response header that tells our browser it is OK to allow this Ajax call:

HTML5 Rocks

Clicking the "Fetch NY Times" button, conversely, doesn't work: the jQuery method .fail(), which we chain on to the end of the $.ajax call to http://www.nytimes.com/, generates a popup alert. If we inspect the response headers, we would find no Access-Control-Allow-Origin: * among them. If we check the console, we find that our browser complains of our attempt to violate the same origin policy:

NY Times

In large part, CORS depends on the server responding with the appropriate headers; if that is the case then, conveniently, our work as client-side developers becomes relatively easy, pretty much the same as if we were working with resources on our own server.

Let's have you try out a call to a remote data source that sends back CORS-enabled headers.

JSONP

JSON with Padding, or JSONP, exploits a loophole in the same-origin policy which browsers employ to prevent access to resources passed via scripts from foreign sites. Instead of passing JSON-formatted data back in an Ajax call, as we do when using Ajax from within our own site, the JSONP response instead returns the JSON-formatted data as the argument of a callback function - "padded" (the "P" in JSONP) by the callback function. Most of the time, our JSONP call to the external resource specifies the name we want for the callback function; on our end, as we receive the "padded" JSON-formatted data, we then invoke the function to process the data. Without this padding - without wrapping the JSON data in a callback function - the security policies in place in our browsers would not allow us to access the foreign resources.

As we've seen previously in this course, a non-JSONP Ajax call might result in the following data returned from another page on our own domain:

{ name: 'Nat' }

With JSONP, the return content might look as such:

callbackfunction({ name: 'Nat' });

On our end, we treat the returned data as a call to callbackfunction() and, if we define what callbackfunction() should do, then we can make use of the JSON-formatted data returned by the foreign server. For example:

callbackfunction = function(data){
  alert(data.name);
};

would popup an alert displaying "Nat", since the result of our Ajax call invokes callbackfunction with a JavaScript object with name name and value Nat.

jQuery makes using JSONP quite easy, abstracting away some of the internal aspects of the remote calls, so we'll use jQuery. We'll again use jQuery's $.ajax method; the only difference between the earlier examples and our use with JSONP are two parameters for the $.ajax method:

  • The jsonp parameter gives the name of the callback function we wish to receive as "padding" around the returned data; for instance, jsonp: "callback".
  • The dataType parameter - which we used before - will now be jsonp.

Let's look at an example using a JSONP call to Yahoo! using their Query Language service.

Code Sample:

CORSJSONP/Demos/jsonp-yahooquery.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>JSONP - Yahoo! Query Language</title>
<script type="text/javascript" src="jquery-1.11.1.min.js"></script>
<script type="text/javascript">
	$(document).ready(function() {
		$('#btn').click(function() {
			var zip = $('#zip').val();
			var query = $('#query').val();
			$.ajax({
				url: "http://query.yahooapis.com/v1/public/yql",
				jsonp: "callback",
				dataType: "jsonp",
				data: {
					q: "select * from local.search where zip='" + zip + "' and query='" + query + "'",
					format: "json"
				},
				success: function(response) {
					$('#Content').html('');
					$.each(response.query.results.Result, function(index, element) {
						$('#Content').append('<p><a href="' + element.MapUrl + '" target="_blank">' + element.Title + '</a></p>');
					});
				}
			});
		});
	});
</script>
</head>
<body>
	<input type="text" id="zip" placeholder="zip"> <input type="text" id="query" placeholder="query (e.g. pizza)">
	<button id="btn">Go</button>
	<div id="Content"></div>
</body>
</html>

Note that we need not start up the Node.js server here, since we are accessing remote data; simply open the file CORSJSONP/Demos/jsonp-yahooquery.html in your browser directly.

Enter a zip code and a type of establishment for which to search (e.g. "pizza", "movies", "supermarkets").

We add a jQuery click handler on the button: when the button is clicked, three things happen:

  1. Local variable zip is assigned the value entered in the #zip text field.
  2. Local variable query is assigned the value entered in the #query text field.
  3. A JSONP Ajax call is sent to the the Yahoo! server, and the results processed.

For the $.ajax call, we specify http://query.yahooapis.com/v1/public/yql as the url, callback is given as the name for the jsonp callback function (as required by this Yahoo! service), and dataType: "jsonp" tells the function that we expect to receive a JSONP-formatted result.

The Yahoo! Query Language service offers a variety of available resources: we choose here to ask for "local" results (usually businesses). As such, as we pass - via the data parameter, a query that sends along the user-supplied zip code and query. We might, for instance, ask for all "pizza" places near a zip code like "13214" using the query select * from local.search where zip='13214' and query='pizza'.

In the success callback function - a parameter of the $.ajax method - we define how we will handle the response. Key here is to discover the schema by which the results are returned. Either by using a tool like Firefox's or Chrome's inspector, or by using Yahoo!'s Query Language console (or both), we find that the set of results in which we are interested are returned in an array "query.results.Result"; thus, our code iterates ($.each) over response.query.results.Result, appending a paragraph to #Content for each returned result. Here's a screenshot from entering that query into the page https://developer.yahoo.com/yql/

Returned Result

The next exercise asks you to try out using JSONP with jQuery.

CORS Vs. JSONP Differences

  • CORS is the more modern of the two approaches to cross-origin resource sharing.
  • CORS supports a variety of HTTP requests; JSONP supports only GET requests.
  • Additionally, CORS allows us to use a regular XMLHttpRequest, which offers better error handling than does JSONP.
  • Conversely, more (older) web browsers support JSONP than do CORS.

Retrieving Country Info from GeoNames via CORS

Duration: 15 to 25 minutes.

In this exercise, you will make an Ajax call to cross-origin site which includes appropriate response headers to enable CORS.

  1. Geonames.org offers a wide range of geographical data, returned in both XML and JSON format. Visit http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country=US&username=webucator&style=full to view the response from the resource we will access.
  2. Note that the response includes the Access-Control-Allow-Origin: * header: Country Info
  3. Note, too, the schema of the response: a field geonames contains an array (n this case with a single element) which contains fields with information about the given country: countryName, population, etc.
  4. Open CORSJSONP/Exercises/cors-geonames.html in your editor; you'll write the code here:
    • Use jQuery to get the value of the #countryabbr input field: this is the two-character country abbreviation - "US" for the United States, "CN" for China, "DE" for Germany, etc.
    • Complete the body of the jQuery call to $.ajax:
      • The type of this Ajax call should be get.
      • The call should go to http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country=XX&username=webucator&style=full, where "XX" is the two-character country abbreviation supplied by the user.
      • In the success callback, check if the country code was valid by checking the length of the returned response; if there was a response with valid data, then display on the page the country's name, population, and any other fields you wish. If not - that is, if response.geonames.length == 0, then display an error message.
  5. Test your solution in a browser by visiting CORSJSONP/Exercises/cors-geonames.html

Solution:

CORSJSONP/Solutions/cors-geonames.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>CORS - Geonames</title>
<style>
	#ResponseContent {
		width:66%;
		padding:1% 2%;
		background-color:#ccc;
		margin-top:20px;
	}
	h2 {
		margin:0;
		padding:0;
	}
</style>
<script type="text/javascript" src="jquery-1.11.1.min.js"></script>
<script type="text/javascript">
	$(document).ready(function() {
		$('#btn').click(function() {
			$('#ResponseContent h2').html('Response Content');
			$('#ResponseContent div').html('');
			var country = $('#countryabbr').val();
			$.ajax({
				type: 'GET',
				url: 'http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country=' + country + '&username=webucator&style=full',
				success: function(response) {
					if (response.geonames.length > 0) {
						$.each(response.geonames, function(index, element) {
							$('#ResponseContent div').append('<p>');
							$('#ResponseContent div').append('Name: ' + element.countryName + '<br>');
							$('#ResponseContent div').append('Currency Code: ' + element.currencyCode + '<br>');
							$('#ResponseContent div').append('Capital City: ' + element.capital + '<br>');
							$('#ResponseContent div').append('Population: ' + element.population + '<br>');
							$('#ResponseContent div').append('</p>');
						});
					} else {
						$('#ResponseContent div').append('<p><em>Country not found</em></p>');
					}
				}
			});
		});
	});
</script>
</head>
<body>
	<input type="text" id="countryabbr" placeholder="US">
	<button id="btn">Fetch Country Info</button>
	<br>
	<div id="ResponseContent">
		<h2>Response Content</h2>
		<div></div>
	</div>
</body>
</html>

Our Ajax calls goes out to

http://api.geonames.org/countryInfoJSON?formatted=true&lang=en&country='+country+'&username=webucator&style=full'

where country is the user-entered value from the text field.

We check that the country code we send along in our Ajax call results in a valid response; if so, then we process the response by iterating, using jQuery's $.each method, over the response array response.geonames, appending to $('#ResponseContent div') some information about the country. If the response is empty, then we display "Country not found".

Retrieving Place Information from Yahoo! with JSONP

Duration: 15 to 25 minutes.

In this exercise, you will put to use the JSONP concepts we reviewed previously, using jQuery to retrieve information about a user-entered place.

  1. Visit https://developer.yahoo.com/yql/
    • Enter the following as the YQL Query: select * from geo.places where text='sfo'
    • Choose response type "JSON" and click the "Test" button.
    • Yahoo! returns information about "sfo" - the San Francisco airport in California, US.
    • Check the schema for the returned data: the top-level field is query, which contains an object with field results, which in turn contains a field place.
    • For this query ("sfo") the returned field place contains a number of fields like placeTypeName, whose field content tells us that this is an "Airport", and name, which tells us the full name of "sfo".
    • If we change the query to select * from geo.places where text='13066' - a United States zip code - we see that the query.results.place is now an array: we get results for US zip code "13066" but also for Mexican postal code "130", Brazilian postal code "13066", etc. We'll need to handle this fact - that Yahoo! will sometimes return an array of places and sometimes just a single place.
  2. Open CORSJSONP/Exercises/jsonp-yahooquery.html; you'll write the code here:
    • Use jQuery to get the value of the #text input field.
    • Complete the body of the jQuery call to $.ajax:
      • Add appropriate name/value parameter pairs for url, jsonp, and dataType; refer back to the earlier example for reference, if needed.
      • Add a success callback, which should:
        • Test the returned data to see if Array.isArray(response.query.results.place) - that is, whether the returned results include an array of places or just one place
        • Either iterate over the returned results (if there is an array) or find the single relevant fields (if there is not an array) and append a paragraph (or paragraphs) to div#Content.
    • Test your work in a browser.
  3. Test your solution in a browser by visiting CORSJSONP/Exercises/jsonp-yahooquery.html

Solution:

CORSJSONP/Solutions/jsonp-yahooquery.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>JSONP - Yahoo! Query Language</title>
<script type="text/javascript" src="jquery-1.11.1.min.js"></script>
<script type="text/javascript">
	$(document).ready(function() {
		$('#btn').click(function() {
			var text = $('#text').val();
			$.ajax({
				url: "http://query.yahooapis.com/v1/public/yql",
				jsonp: "callback",
				dataType: "jsonp",
				data: {
					q: "select * from geo.places where text='" + text + "'",
					format: "json"
				},
				success: function(response) {
					$('#Content').html('');
					if (Array.isArray(response.query.results.place)) {
						$.each(response.query.results.place, function(index, element) {
							$('#Content').append('<p>' + element.placeTypeName.content + ': ' + element.name + ', ' + 'country: ' + element.country.content + '>/p>');
						});
					} else {
						$('#Content').append('<p>' + response.query.results.place.placeTypeName.content + ': ' + response.query.results.place.name + ', ' + 'country: ' + response.query.results.place.country.content + '</p>');
					}
				}
			});
		});
	});
</script>
</head>
<body>
	<input type="text" id="place" placeholder="place">
	<button id="btn">Go</button>
	<div id="Content"></div>
</body>
</html>

We get the user-entered place name with the code var place = $('#place').val();

We set the following parameters in our call to $.ajax:

  1. url: "http://query.yahooapis.com/v1/public/yql" - the URL to which we'll send the remote request.
  2. jsonp: "callback" - the name of the callback function for our JSONP call.
  3. dataType: "jsonp" - the type of data we expect to be returned.

In the success callback, we first set the contents of div#Content to be empty (.html('')), so that repeated clicks of the button "wipe out" any previous displayed data.

We then check to see if we get an array returned; if so, we iterate (with $.each) over the array response.query.results.place of returned results, each time appending a new paragraph to div#Content with the type (element.placeTypeName.content) and name (element.name) and country (element.country.content) of the place. If there is no array, we append a single paragraph with the results.