I’ve been using jQuery for doing AJAX requests and I thought all was good. After-all, one of the advantages of using a library is they do all of the hard thinking for me and I just need to implement their methods correctly, right? ;)
Well, I ran into an edge case that has been bugging me for some time, which was causing the AJAX requests to fail for about 1%-2% of my users, but I couldn’t replicate the issue on my end in any way…
Contents
The Problem
What makes my situation unique, is that I’m calling to a NodeJS server in which the server is accessed only via IP address (thus a different domain than the client requested the initial page) and a non-standard port. Now technically speaking, the jQuery AJAX request is still working correctly and doing what it should. But what I found as I point out below, there are hidden catches to this methodology that make it the incorrect solution.
Keep in mind, this article is talking about Cross Origin Resource Sharing (CORS) requests only. In other words, its an AJAX request in which the URL of the web page that the client originally loaded is at a different origin (domain) than that of the URL that the AJAX request is making. There are three pieces that can make this a CORS request:
- Protocol – HTTP vs HTTPS
- Domain – example.com vs test.com
- Port – Port 80 (not needed in most URLS) vs port 5555
So to further illustrate, these are examples of URLS that fall into a CORS scenario:
- http://www.example.com -> http://example.com
- http://www.example.com -> https://www.example.com
- http://www.example.com -> http://www.example.com:5555
- http://www.example.com -> http://123.456.789.101
- http://www.example.com -> http://123.456.789.101:5555
The 95% Solution
Real quick, here is the scenario I had built using jQuery and my NodeJS server. NOTE – You don’t need to know anything about NodeJS to understand the principles here. The moral of the story though is that I used NodeJS to build a webserver for this web application function. So I am in full control of both the client and server side code bases.
- Users on my website, http://example.com, would make a request to my NodeJS server at http://123.456.789.101:5555 (IP address and port number).
- I’m using jQuery AJAX via json to process this request
- My NodeJS server processes an OPTIONS request to pre-flight the request, and send the access-control-allow-origin header. Based on my research, this combination is enough for proper CORS scripting.
My AJAX code looked like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var url = 'http://123.456.789.101:5555?some=thing&more=info'; jQuery.ajax({ crossDomain: true, url: url, dataType: 'json', /* CORS Method */ contentType: "application/json; charset=utf-8;", cache: false, success: function( data ) { processFinalImageOutputSuccess( JSON.stringify( data ) ); }, error: function(xhr, status, errorThrown) { processFinalImageOutputError("003", xhr, status, errorThrown); } }); |
My NodeJS server would send back an access-control-allow-origin header response
1 2 3 4 5 6 7 8 9 10 11 12 | response.writeHead( "204", "No Content", { "access-control-allow-origin": origin, "access-control-allow-methods": "GET, POST, OPTIONS", "access-control-allow-headers": "content-type, accept", "access-control-max-age": 10, // Seconds. "content-length": 0 } ); return( response.end() ); |
So what was actually happening here is that the server will receive the request and processes it. But for the case of 1%-2% my visitors, they would not receive a response, and the jQuery AJAX function would process the error function instead of the success function.
Determining There are Flaws…
I should explain quick how I knew that I had some users that were experiencing issues. I used several methods to find this issue (as it’s hard to test when you don’t see the problem).
- I have a customer service team that gets the phone calls when this system fails – and thus they alerted me to the issue. However, me communicating through my Customer Service team who is either on the phone or online chat with an aggravated customer isn’t the best way to find out what the scenario is that is causing the issue. So I knew there was a problem, but not much insight as to what the cause was.
- Google Analytics. I implemented event tracking through Google Analytics that was triggered in my jQuery AJAX Success and Failure events. This again gave me insight to the severity of the issue, but not the source of where the true issue is.
- My Wife. I was excited to launch the new version of my web app, so I shared it with my wife, who works in a school. She tried it out and pointed out to me that it didn’t work. I frantically test it, and it does for me. I monitor my analytics, I try different configurations in my websites firewall, I ask her to try again, and it still fails. So what’s going on?!?! Well the unique factor here is that she is in a school where social media websites and some others are blocked. So this tells me there is advanced security setup that is not in my configuration.
I did some research on the issue, tried some more changes, and still couldn’t come up with a solution. So I did what every web developer should do (well maybe) when they are frustrated and have no clue – I ignored it and hoped that it would either magically fix it self or people would just not say anything to me anymore. Good solution, right? ;)
Ok, but after a while I revisited the issue and came up with
The 100% Solution
What finally set me in the right direction was a random Google search that lead to a random blog post (who reads those, seriously? ha!) that in my opinion is not stated enough in the internets AJAX / CORS conversations – a post titled Don’t use Access-Control-Allow-Origin. (I want to say a huge thank you to Gary Sieling for posting this.)
So the problem is this, and I quote from Gary:
1 2 | <em><code>The real problem is entirely outside your control – corporate firewall proxies. The Watchguard Firewall is very aggressive by default, blocking content on a variety of heuristics. It removes HTTP headers it considers dangerous, including Access-Control-Allow-Origin, so a site built with this will never work for anyone inside their firewall. </code></em> |
Simple as that – I can build the best web app with the best written AJAX methods jQuery offers (or manually for that matter) and yet corporate (or school, etc.) firewalls will stop my work dead in it’s tracks – all because I’m trying to do a cross origin request.
This lead me to alternative methods to perform AJAX requests, and I came across the article Cross-Domain requests in Javascript by JVANEYCK where he outlines alternative methods to perform this task. I found the most logical for my situation to be a Server Side Proxy.
Under this method, the Javascript AJAX request from a webpage at http://example.com sends a request to a PHP script that is on the same domain, http://example.com. This request is then not a cross-origin request. The PHP script then makes a server-side call using PHP to the second domain, http://123.456.789.101:5555. And this is where the critical difference is that I was not thinking about – server side scripting can make a cross origin call without any issues – it’s only client to server calls that need special consideration on how to perform a cross origin request. So now the client on domain A talks to the server on domain A, which talks to the server on domain B, which sends a response to the server on domain A, and sends the response back to the client on domain A. And at the end of all of this, ALL of your web app users have full access to the functionality they expect!
Client Code
1 2 3 4 5 6 7 8 9 10 11 12 | $.ajax({ url: urlServerProxy, dataType: 'json', /* CORS Method */ contentType: "application/json; charset=utf-8;", cache: false, success: function( data ) { processFinalImageOutputSuccess( JSON.stringify( data ) ); }, error: function(xhr, status, errorThrown) { processFinalImageOutputError("003", xhr, status, errorThrown); } }); |
Server Proxy Code
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php $jsonurl = "http://123.456.789.101:5555/?".$_SERVER['QUERY_STRING']; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $jsonurl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true ); $result = curl_exec($ch); if( !$result ) { die('Error: "' . curl_error($ch) . '" - Code: ' . curl_errno($ch)); } curl_close($ch); echo $result; ?> |
Note: I used CURL to call to make the request to the cross-domain URL. You could also use the PHP file_get_contents() function if you’d prefer.
Another Note: Internet Explorer doesn’t always work with the jQuery AJAX function under CORS. IE9 and older are the particular versions that don’t work. For those browsers, you need to do some more manual xdr requests, which I discuss in this article, which means you need to detect the browser to implement the proper method of AJAX. HOWEVER the server proxy method outlined above alleviates this need because you are bypassing browser CORS requests all together.
Conclusion
Now that I’ve been through this whole process, I’m learning that jQuery AJAX CORS is NOT reliable due to network hardware that is out of a developers control and will avoid it like the plague when making CORS request. The server side proxy is certainly the way to go for accomplishing CORS requests when you are in control of both the server and the client code sets.
I hope this post helps you out – feel free to comment with any suggestions or modifications you might have.