CORS/JSONP: Accessing Remote Sites

Contact Us or call 1-877-932-8228
CORS/JSONP: Accessing Remote Sites

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 XHR/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 any of the following demos and exercises.

Code Sample:

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

<script>

	window.onload = function() {
		var btnhtml5rocks = document.getElementById('btnhtml5rocks');
		var responseContent = document.getElementById('responseContent');
		btnhtml5rocks.addEventListener('click', function(e) {
			responseContent.innerHTML = '<h2>Response Content</h2>';
			
			var xmlhttp = new XMLHttpRequest();

			xmlhttp.open("GET", "https://www.html5rocks.com/en/tutorials/file/xhr2/", true);
			xmlhttp.onreadystatechange = function() {
				if (xmlhttp.readyState == 4) {
					if (xmlhttp.status == 200) {
						responseContent.innerHTML += xmlhttp.responseText;
					} else {
						alert("failed!");
					}
				}
			}
			xmlhttp.send(null);
		});

		btnnytimes.addEventListener('click', function(e) {
			responseContent.innerHTML = '<h2>Response Content</h2>';;
			
			var xmlhttp = new XMLHttpRequest();

			xmlhttp.open("GET", "http://www.nytimes.com/", true);
			xmlhttp.onreadystatechange = function() {
				if (xmlhttp.readyState == 4) {
					if (xmlhttp.status == 200) {
						responseContent.innerHTML += xmlhttp.responseText;
					} else {
						alert("failed!");
					}
				}
			}
			xmlhttp.send(null);
		});
	};
</script>
</head>
<body>
	<button id="btnhtml5rocks">Fetch HTML5 Rocks</button>
	<button id="btnnytimes">Fetch NY Times</button>
	<br>
	<div id="responseContent"></div>
</body>
</html>

Code Explanation

The page presents two buttons, with ids btnhtml5rocks and btnnytimes, respectively; we add a click handler to each button

When either button is clicked, we make an Ajax call to a remote site, setting the contents of #responseContent div, if successful in our Ajax call, to the contents received as the response.

Clicking the "Fetch HTML5 Rocks" button generates an Ajax call to https://www.html5rocks.com/en/tutorials/file/xhr2/. We display the contents of the response 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: we get no response and, thus, generate 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.

We 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 JSONP call invokes callbackfunction with a JavaScript object with name name and value Nat.

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

Code Sample:

XHR/Demos/jsonp-yahooquery.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>JSONP - Yahoo! Query Language</title>
<script>
	function displayInfo(data) {
		var content = document.getElementById('content');
		content.innerHTML = '';
		var results = data.query.results.Result;
		for(i=0; i<results.length;i++) {
			result = results[i];
			content.innerHTML += '<p><a href="' + result.MapUrl + '" target="_blank">' + result.Title + '</a></p>';
		}
	}

	function requestJSONP(url) {
		var script = document.createElement('script');
		script.src = url;
		script.onload = function () {
			this.remove();
		};
		document.head.appendChild(script);
	}

	window.onload = function() {

		var btn = document.getElementById('btn');

		btn.addEventListener('click', function(e) {
			var zip = document.getElementById('zip').value;
			var query = document.getElementById('query').value;
			var script = document.createElement('script');
			var url = "http://query.yahooapis.com/v1/public/yql?format=json&q=select+*+from+local.search+where+zip='" + zip + "'+and+query='" + query + "'&callback=displayInfo"
			requestJSONP(url);
		});
	}
</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>

Code Explanation

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

We add a 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. We create a URL string, appending to it a search query in the format dictated by Yahoo!'s Query Language service, embedding the user-entered zip code and item (like "pizza") into the string.
  4. We initiate a JSONP call:
    • We call function requestJSONP, passing to it our URL string.
    • Function requestJSONP appends a script tag to the head of our page.
    • The callback parameter we added to the URL invokes our callback function displayInfo, to which is passed the JSON response as the data parameter.
    • Function displayInfo processes the returned JSON, iterating over the relevant results and appending each item (each pizza place, for example) to the #content div.

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'.

Pizza Results

The next exercise asks you to try out using JSONP.

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.
Next