Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js

Posted February 10, 2012 at 11:10 AM by Ben Nadel

Tags: Javascript / DHTML

After reading the REST API Design Rulebook by Mark Masse, I'm all jazzed up about API design! This recent enthusiasm has afforded me the motivation to attack a number of new topics all at the same time. If I'm going to be experimenting with API design, why not try it on an Amazon EC2 (Elastic Compute Cloud) Micro instance? And if it's on EC2, why not try it using Node.js? And, if it's on a remote server, why not look into how Cross-Origin Resource Sharing (CORS) works with jQuery and remote REST APIs? This last question - cross-domian AJAX requests - is what I'd like to touch on in this post.


 
 
 

 
  
 
 
 

While I've done a ton of work with AJAX (Asynchronous JavaScript and XML), I've never actually done anything with CORS (Cross-Origin Resource Sharing). Many months ago, Sagar Ganatra - an awesome Adobe ColdFusion Engineer - talked about cross-origin requests with the XmlHTTPRequest2 object. This was one of my first views into the possibility of cross-domain AJAX requests; sadly, my exploration ended there.

Until yesterday!

CORS (Cross-Origin Resource Sharing) is both simple and complex. It is quite simple in philosophy; but, the devil, as always, is in the details. I need to give a huge thank-you to Nicholas Zakas and Remy Sharp for clearing up the CORS workflow on their respective blogs.

CORS is AJAX. What makes CORS special is that the AJAX request is being posted to a domain different than that of the client. Historically, this type of request has been deemed a security threat and has been denied by the browser. With the prevalence of AJAX and the transformation of thick-client applications, however, modern browsers have been evolved to embrace the idea that critical information doesn't necessarily come from the host domain.

Now, modern browsers (Internet Explorer 8+, Firefox 3.5+, Safari 4+, and Chrome) can make AJAX requests to other domains so long as the target server allows it. This security handshake takes place in the form of HTTP headers. When the client (browser) makes cross-origin requests, it includes the HTTP header - Origin - which announces the requesting domain to the target server. If the server wants to allow the cross-origin request, it has to echo back the Origin in the HTTP response heder - Access-Control-Allow-Origin.

NOTE: The server can also echo back "*" as the Access-Control-Allow-Origin value if it wants to be more open-ended with its security policy.

Depending on the complexity of the cross-origin request, the client (browser) may make an initial request - known as a "preflight" request - to the server to gather authorization information. This preflight request can be cached by the client and is therefore not needed for subsequent CORS requests.

NOTE: Not every type of CORS request requires a preflight request; furthermore, some browsers do not support this concept.

Rather than diving deeper into the actual HTTP request headers (on which I am not an authority), I'd rather just get into some code that makes use of CORS (Cross-Origin Resource Sharing) AJAX requests.

Before we look at the Node.js server, let's take a look at the client-side jQuery code that performs the CORS requests. In the following code, I'm making both a PUT and a DELETE request to the Node.js server. Notice that I am chaining these two requests in serial; this allows the preflight request to be made on the first CORS request but not on the second:

demo.htm - Our jQuery-Powered CORS Demo

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Cross-Origin Resource Sharing (CORS) With jQuery And Node.js</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Cross-Origin Resource Sharing (CORS) With jQuery And Node.js
  • </h1>
  •  
  •  
  • <h2>
  • PUT Response
  • </h2>
  •  
  • <pre id="putResponse">
  • <!-- To be populated dynamically. -->
  • </pre>
  •  
  •  
  • <h2>
  • DELETE Response
  • </h2>
  •  
  • <pre id="deleteResponse">
  • <!-- To be populated dynamically. -->
  • </pre>
  •  
  •  
  •  
  • <!-- Load our JavaScript and make some CORS requests. -->
  • <script type="text/javascript" src="../jquery-1.7.1.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Wrap up the PUT request execution.
  • var makePUTRequest = function(){
  •  
  • // Make the PUT request.
  • $.ajax({
  • type: "PUT",
  • url: "http://localhost:8080/some/url/resource/path",
  • contentType: "application/json",
  • data: JSON.stringify({
  • name: "Tricia",
  • age: 37
  • }),
  • dataType: "text",
  • success: function( response ){
  •  
  • // Put the plain text in the PRE tag.
  • $( "#putResponse" ).text( response );
  •  
  • },
  • error: function( error ){
  •  
  • // Log any error.
  • console.log( "ERROR:", error );
  •  
  • },
  • complete: function(){
  •  
  • // When this completes, execute teh
  • // DELETE request.
  • makeDELETERequest();
  •  
  • }
  • });
  •  
  • };
  •  
  •  
  • // Wrap up the DELETE request execution so it can easily be
  • // invoked from the end of the PUT delete response.
  • var makeDELETERequest = function(){
  •  
  • // Make the DELETE request.
  • $.ajax({
  • type: "DELETE",
  • url: "http://localhost:8080/some/url/resource/path",
  • contentType: "application/json",
  • data: JSON.stringify({
  • name: "Tricia",
  • age: 37
  • }),
  • dataType: "text",
  • success: function( response ){
  •  
  • // Put the plain text in the PRE tag.
  • $( "#deleteResponse" ).text( response );
  •  
  • },
  • error: function( error ){
  •  
  • // Log any error.
  • console.log( "ERROR:", error );
  •  
  • }
  • });
  •  
  • };
  •  
  •  
  • // Execute the PUT request.
  • makePUTRequest();
  •  
  •  
  • </script>
  • </body>
  • </html>

As you can see, there's nothing really special going on here. I'm using jQuery to execute AJAX requests in the same way that I always would. The only difference is that I'm posting the AJAX requests to a different domain (and port number).

When we run the above code, we get the following page output:


 
 
 

 
 CORS AJAX request using jQuery and Node.js. 
 
 
 

As you can see, the PUT request is preceded by an OPTIONS request. This OPTIONS request tells the client if a CORS request will be allowed; and, for those requests, which methods (GET, POST, PUT, etc.) can be executed. This OPTIONS request is cached by the client for a server-defined amount of time. This is why only one OPTIONS request needs to be performed despite that fact that two CORS requests (PUT and DELETE) and being run.

Now that we see how little the client-side code has to change - hardly at all - let's take a look at the Node.js server to see how the security handshake works. For this demo, the Node.js server understands two concepts: 1) OPTIONS requests and 2) everything else. If the request is not an OPTIONS request, the Node.js server simply echos back the request body that was posted.

server.js - Our CORS-Enabled Node.js Server

  • // Include our HTTP module.
  • var http = require( "http" );
  •  
  •  
  • // Create an HTTP server so that we can listen for, and respond to
  • // incoming HTTP requests. This requires a callback that can be used
  • // to handle each incoming request.
  • var server = http.createServer(
  • function( request, response ){
  •  
  •  
  • // When dealing with CORS (Cross-Origin Resource Sharing)
  • // requests, the client should pass-through its origin (the
  • // requesting domain). We should either echo that or use *
  • // if the origin was not passed.
  • var origin = (request.headers.origin || "*");
  •  
  •  
  • // Check to see if this is a security check by the browser to
  • // test the availability of the API for the client. If the
  • // method is OPTIONS, the browser is check to see to see what
  • // HTTP methods (and properties) have been granted to the
  • // client.
  • if (request.method.toUpperCase() === "OPTIONS"){
  •  
  •  
  • // Echo back the Origin (calling domain) so that the
  • // client is granted access to make subsequent requests
  • // to the API.
  • response.writeHead(
  • "204",
  • "No Content",
  • {
  • "access-control-allow-origin": origin,
  • "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
  • "access-control-allow-headers": "content-type, accept",
  • "access-control-max-age": 10, // Seconds.
  • "content-length": 0
  • }
  • );
  •  
  • // End the response - we're not sending back any content.
  • return( response.end() );
  •  
  •  
  • }
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // If we've gotten this far then the incoming request is for
  • // our API. For this demo, we'll simply be grabbing the
  • // request body and echoing it back to the client.
  •  
  •  
  • // Create a variable to hold our incoming body. It may be
  • // sent in chunks, so we'll need to build it up and then
  • // use it once the request has been closed.
  • var requestBodyBuffer = [];
  •  
  • // Now, bind do the data chunks of the request. Since we are
  • // in an event-loop (JavaScript), we can be confident that
  • // none of these events have fired yet (??I think??).
  • request.on(
  • "data",
  • function( chunk ){
  •  
  • // Build up our buffer. This chunk of data has
  • // already been decoded and turned into a string.
  • requestBodyBuffer.push( chunk );
  •  
  • }
  • );
  •  
  •  
  • // Once all of the request data has been posted to the
  • // server, the request triggers an End event. At this point,
  • // we'll know that our body buffer is full.
  • request.on(
  • "end",
  • function(){
  •  
  • // Flatten our body buffer to get the request content.
  • var requestBody = requestBodyBuffer.join( "" );
  •  
  • // Create a response body to echo back the incoming
  • // request.
  • var responseBody = (
  • "Thank You For The Cross-Domain AJAX Request:\n\n" +
  • "Method: " + request.method + "\n\n" +
  • requestBody
  • );
  •  
  • // Send the headers back. Notice that even though we
  • // had our OPTIONS request at the top, we still need
  • // echo back the ORIGIN in order for the request to
  • // be processed on the client.
  • response.writeHead(
  • "200",
  • "OK",
  • {
  • "access-control-allow-origin": origin,
  • "content-type": "text/plain",
  • "content-length": responseBody.length
  • }
  • );
  •  
  • // Close out the response.
  • return( response.end( responseBody ) );
  •  
  • }
  • );
  •  
  •  
  • }
  • );
  •  
  •  
  • // Bind the server to port 8080.
  • server.listen( 8080 );
  •  
  •  
  • // Debugging:
  • console.log( "Node.js listening on port 8080" );

As you can see, the Node.js server either responds with a 204 No Content response for OPTIONS requests; or, it responds with a 200 OK response for everything else.

As far as I'm concerned, this is some pretty cool stuff! CORS (Cross-Origin Resource Sharing) AJAX requests may not be supported by every modern browser; but, if you're working on API design, like I am, client-side support is not necessarily a must-have - rather, it's a nice-to-have. And, with such little overhead, it's not too hard to implement.




Reader Comments

Feb 10, 2012 at 11:30 AM // reply »
54 Comments

After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/


Apr 28, 2012 at 5:31 AM // reply »
1 Comments

good post; well presented.
Can you explain how this works in internet explorer 8+? I thought the IE implementation of the Wc3 Recommedation was (typically) non-conformist and requires us to create a XDomainRequest object?


Apr 28, 2012 at 10:26 PM // reply »
1 Comments

Note: anyone silly enough to use IE will need to

  • jQuery.support.cors = true;

in the demo.htm before the first ajax call, as CORS is disabled when run from a localhost.
ref. http://bugs.jquery.com/ticket/8122


May 8, 2012 at 10:46 AM // reply »
10,743 Comments

@Drew,

Awesome link, thanks!

@Cubic,

Unfortunately (or fortunately), I'm on a Mac, so I tend not to test too much R&D code in IE as it would require me booting up my Virtual Machine. Thanks for the link.

In the api.jquery.com site, it looks like you can add the "withCredentials" header to the AJAX xhrFields collection to get jQuery to honor this:

http://api.jquery.com/jQuery.ajax/

... look under "xhrFields" (the last property).


May 9, 2012 at 3:45 AM // reply »
1 Comments

Hi, Ben
Thank you for the awesome post. It really worked for me.
I tried to make a HTTP POST request using CORS but it was not making HTTP OPTIONS request. After researching i found that POST and GET requests will not make any OPTIONS request for handshake, unless you have a custom headers like X-PINGOTHER.
More information is here
https://developer.mozilla.org/En/HTTP_access_control#Preflighted_requests

Thanks a lot :)


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 21, 2012 at 1:58 AM
Updated: Converting A ColdFusion Query To CSV Using QueryToCSV()
Hi Ben, why do you need to have so many double quotes when adding the field and field name to the row data? ----------------------------------------- <cfset LOCAL.RowData[ LOCAL.ColumnIndex ] = ... read »
AXL
May 21, 2012 at 1:24 AM
URL Rewriting And ColdFusion's WriteToBrowser Image Functionality (CFFileServlet)
@Mounir, Open your lower case URL Rewrite rule and add the following condition. Condition input: {REQUEST_URI} Check if input string: Does Not Match the Pattern Pattern: ^/CFFileServlet/_cf_ca ... read »
May 20, 2012 at 4:28 AM
Understanding The Complex And Circular Relationships Between Objects In JavaScript
@Will Vaughn I tried your javascript example but got this error:- foo.print is not a function ... read »
May 19, 2012 at 5:37 AM
A Graphical Explanation Of Javascript Closures In A jQuery Context
Thanks for this article, but I fear you missed an important point. If variables in the outer context change, these changes affect the inner anonymous functions as well. That means: if you change the ... read »
May 18, 2012 at 3:39 PM
Parsing CSV Data With An Input Stream And A Finite State Machine
Can you use file upload button with this? and read live? or does the file have to already be on the server saved? ... read »
May 18, 2012 at 1:06 AM
VIRGO (Aug. 23-Sept. 22): Dead On The Money!
A friend of mine and I were arguing about astrology and she told me that he believes in astrology. She hasn't provided me with any evidence that the belief makes any sense to me. She she been telling ... read »
May 17, 2012 at 11:32 PM
Using ColdFusion to Handle 404 Errors (Page Not Found) On Development Server
Very easy the configuration. I read a lot pages and I can't find the solution. I open the administrator and change this Administrator/server settings/Error Handlers/Missing Template Handler and p ... read »
May 17, 2012 at 3:13 PM
LOCAL Variables Scope Conflicts With ColdFusion Query of Queries
I never cease to be amazed that almost EVERY random CF issue I come across lands me on your site. Thank you for documenting your findings for the world. ... read »