CDNs fail, but your scripts don't have to - fallback from CDN to local jQuery
There's a great website called http://whoownsmyavailability.com that serves as a reminder to me (and all of us) that external dependencies are, in fact, external. As such, they are calculated risks with tradeoffs. CDNs are great, but for those minutes or hours that they go down a year, they can be super annoying.
I saw a tweet today declaring that the ASP.NET Content Delivery Network was down. I don't work for the CDN team but I care about this stuff (too much, according to my last performance review) so I turned twitter to figure this out and help diagnose it. The CDN didn't look down from my vantage point.
I searched for things like "ajax cdn,"microsoft cdn," and "asp.net cdn down" and looked at the locations reported by the Twitter users in their profiles. They had locations like CT, VT, DE, NY, ME. These are all abbreviations for states in the northeast of the US. There were also a few tweets from Toronto and Montreal. Then, there was one random tweet from a guy in Los Angeles on the other side of the country. LA doesn't match the pattern that was developing.
I tweeted LA guy and asked him if he was really in LA or rather on the east coast.
@shanselman @attiladelisle Oh weird. It's back for me because I'm in CA and I unplugged my persistent VPN to MA :)
— Alex Whittemore (@alexwhittemore) April 30, 2013
Bingo. He was VPN'ed into Massachusetts (MA). I had a few folks send me tracerts and sent them off to the CDN team who fixed the issue in a few minutes. There was apparently a bad machine in Boston/NYC area that had a configuration change specific to the a certain Ajax path that had gone undetected by their dashboard (this has been fixed and only affected the Ajax part of the CDN in this local area).
More importantly, how can we as application developers fallback gracefully when an external dependency like a CDN goes down? Just last week I moved all of my Hanselminutes Podcast images over to a CDN. If there was a major issue I could fall back to local images with a code change. However, if this was a mission critical site, I should not only have a simple configuration switch to fallback to local resources, but I should also test and simulate a CDN going down so I'm prepared when it inevitably happens.
With JavaScript we can detect when our CDN-hosted JavaScript resources like jQuery or jQuery UI aren't loaded successfully and try again to load them from local locations.
Falling back from CDN to local copies of jQuery and JavaScript
The basic idea for CDN fallback is to check for a type or variable that should be present after a script load, and if it's not there, try getting that script locally. Note the important escape characters within the document.write. Here's jQuery:
<script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js"></script>
<script>
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='/js/jquery-2.0.0.min.js' type='text/javascript'%3E%3C/script%3E"));
}
</script>
Or, slightly differently. This example uses protocol-less URLS, checks a different way and escapes the document.write differently.
<script src="//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js"></script>
<script>window.jQuery || document.write('<script src="js/jquery-2.0.0.min.js">\x3C/script>')</script>
If you are loading other plugins you'll want to check for other things like the presence of specific functions added by your 3rd party library, as in "if (type of $.foo)" for jQuery plugins.
Some folks use a JavaScript loader like yepnope. In this example you check for jQuery as the complete (loading) event fires:
yepnope([{
load: 'http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js',
complete: function () {
if (!window.jQuery) {
yepnope('js/jquery-2.0.0.min.js');
}
}
}]);
Even better, RequireJS has a really cool shorthand for fallback URLs which makes me smile:
requirejs.config({
enforceDefine: true,
paths: {
jquery: [
'//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min',
//If the CDN location fails, load from this location
'js/jquery-2.0.0.min'
]
}
});
//Later
require(['jquery'], function ($) {
});
With RequireJS you can then setup dependencies between modules as well and it will take care of the details. Also check out this video on Using Require.JS in an ASP.NET MVC application with Jonathan Creamer.
Updated ASP.NET Web Forms 4.5 falls back from CDN automatically
For ASP.NET Web Forms developers, I'll bet you didn't know this little gem. Here's another good reason to move your ASP.NET sites to ASP.NET 4.5 - using a CDN and falling back to local files is built into the framework.
(We've got this for ASP.NET MVC also, keep reading!)
Fire up Visual Studio 2012 and make a new ASP.NET 4.5 Web Forms application.
When using a ScriptManager control in Web Forms, you can set EnableCdn="true" and ASP.NET will automatically change the <script> tags from using local scripts to using CDN-served scripts with local fallback checks included. Therefore, this ASP.NET WebForms ScriptManager:
<asp:ScriptManager runat="server" EnableCdn="true">
<Scripts>
<asp:ScriptReference Name="jquery" />
<asp:ScriptReference Name="jquery.ui.combined" />
</Scripts>
</asp:ScriptManager>
...will output script tags that automatically use the CDN and automatically includes local fallback.
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(window.jQuery)||document.write('<script type="text/javascript" src="Scripts/jquery-1.8.2.js"><\/script>');//]]>
</script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.24/jquery-ui.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(!!window.jQuery.ui && !!window.jQuery.ui.version)||document.write('<script type="text/javascript" src="Scripts/jquery-ui-1.8.24.js"><\/script>');//]]>
</script>
What? You want to use your own CDN? or Googles? Sure, just make a ScriptResourceMapping and put in whatever you want. You can make new ones, replace old ones, put in your success expression (what you check to make sure it worked), as well as your debug path and minified path.
var mapping = ScriptManager.ScriptResourceMapping;
// Map jquery definition to the Google CDN
mapping.AddDefinition("jquery", new ScriptResourceDefinition
{
Path = "~/Scripts/jquery-2.0.0.min.js",
DebugPath = "~/Scripts/jquery-2.0.0.js",
CdnPath = "http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js",
CdnDebugPath = "https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.js",
CdnSupportsSecureConnection = true,
LoadSuccessExpression = "window.jQuery"
});
// Map jquery ui definition to the Google CDN
mapping.AddDefinition("jquery.ui.combined", new ScriptResourceDefinition
{
Path = "~/Scripts/jquery-ui-1.10.2.min.js",
DebugPath = "~/Scripts/jquery-ui-1.10.2.js",
CdnPath = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js",
CdnDebugPath = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.js",
CdnSupportsSecureConnection = true,
LoadSuccessExpression = "window.jQuery && window.jQuery.ui && window.jQuery.ui.version === '1.10.2'"
});
I just do this mapping once, and now any ScriptManager control application-wide gets the update and outputs the correct fallback.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(window.jQuery)||document.write('<script type="text/javascript" src="Scripts/jquery-2.0.0.js"><\/script>');//]]>
</script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(window.jQuery && window.jQuery.ui && window.jQuery.ui.version === '1.10.2')||document.write('<script type="text/javascript" src="Scripts/jquery-ui-1.10.2.js"><\/script>');//]]>
</script>
If you want to use jQuery 2.0.0 or a newer version than what came with ASP.NET 4.5, you'll want to update your NuGet packages for ScriptManager. These include the config info about the CDN locations. To update (or check your current version against the current) within Visual Studio go to Tools | Library Package Manager | Manage Libraries for Solution, and click on Updates on the left there.
Updated ASP.NET Web Optimization Framework includes CDN Fallback
If you're using ASP.NET MVC, you can update the included Microsoft.AspNet.Web.Optimization package to the -prerelease (as of these writing) to get CDN fallback as well.
Note that I've on the Updates tab within the Manage NuGet Packages dialog but I've selected "Include Prerelease."
Now in my BundleConfig I can setup my bundles to include not only the CdnPath but also a CdnFallbackExpression:
public static void RegisterBundles(BundleCollection bundles)
{
bundles.UseCdn = true;
BundleTable.EnableOptimizations = true; //force optimization while debugging
var jquery = new ScriptBundle("~/bundles/jquery", "//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js").Include(
"~/Scripts/jquery-{version}.js");
jquery.CdnFallbackExpression = "window.jQuery";
bundles.Add(jquery);
//...
}
Regardless of how you do it, remember when you setup Pingdom or other availability alerts that you should be testing your Content Delivery Network as well, from multiple locations. In this case, the CDN failure was extremely localized and relatively short but it could have been worse. A fallback technique like this would have allowed sites (like mine) to easily weather the storm.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
I have blogged about it here jQuery and jQuery UI fallbacks
You can check for the visibility of an element with the class="ui-helper-hidden". If it is visible, then your CSS has not loaded and can fall back to the local version.
In at least the last 10 sites I've built the only one that didn't use a CDN AND Local fallback was for a privately secure site where, due to security levels, we removed the CDN call from an HTML5 Boilerplate based template and only used the local call?
I'm just wondering why anyone wouldn't use both scripting calls as best practices ... I'm confused?
Jon
thank you for great article, just small correction. RequireJS module paths should not end with .js extension.
@Ivan, thank you! I wouldn't have ever called that situation a "circuit breaker", you learn something new every day!
Jon
are any other the additional path options in scriptmanager also available with MVC4's bundle system? i.e. Path, CdnPath,DebugPath, CdnDebugPath. I'm only aware of the first 2.
Cheers
Tim
,
An example is http://stackoverflow.com/a/92819/209136 but instead of showing an error image you could modify the src value to go to the local copy.
Hannes - Pardon? The code is open. Crack open the NuGet packages, they are ZIPs.
Thanks for the usefulness.
-tom
In ASP.NET MVC is already available this feature. You just need to install the pre-release version of Microsoft ASP.NET Web Optimization Framework (now available is a version 1.1.0 Beta 1). More in detail about this can be read in the Howard Dierking's article «New Web Optimization Pre-Release Package on NuGet»
Scary. Speaking from experience, caring too much can get you fired.
Thank you for this article, Scott ... i really appreciate the heads up ... so many gotchas to get us; in his excellent ASP.NET MVC free pluralsight videos*, k. Scott Allen mentions the benefits of CDN but i do not recall him mentioning this hazard ... that's not a criticism of kSA because one can only stuff so much information into a video before it becomes an alternative to L. D. Groban's 87 hour "Cure for insomnia". * http://www.asp.net/mvc "Essential Videos"
one point: <noscript>
http://www.w3.org/TR/html-markup/noscript.html`
http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#the-noscript-element
FWIW, a CDN does nothing at all, ditto workarounds, when scripting itself is turned off ... many developers seem to take a "so what" attitude, a.k.a., blame the end user.
BTW, the TLA CDN in another non-TLA context is used to denote "Canadian currency", ergo, at first glance upon your title, i was wondering why Canadians fail.
/g.
Jus' something ran in my mind when @Paul Chen asked about...
Is it possible to setup multiple fallbacks in ScriptManger? say first MS then Google then code.jquery.com then local?
Yes Paul can follow this way, if he prefers multiple fallback(not recommended by any Pro's), but can play around like this...
1st way is using Microsoft's CDN, the alternate way of 1st condition is to use Google CDN or having a fallback to the native local file system javascript code.
<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.min.js"></script>
<script type="text/javascript">
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js' type='text/javascript'%3E%3C/script%3E"));
}
else if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='/js/jquery-1.4.2.min.js' type='text/javascript'%3E%3C/script%3E"));
}
</script>
Hope this helps...
Cheers!
Guruprasad.
-- An entrenched Ruby on Rails developer
Ian - Glad you liked that!
Nice article, thanks.
A few questions:
1. Would the built-in ScriptManager & Optimization framework CND capabilities work properly in both http & https pages?
2. Am I correct that the Optimization framework could be used in any .NET 4.0 web project, not just MVC as you mentioned?
Why bother with cdn,
I am a rails guy.
So in rails sprockets includes and minifies all js files by default when in production.
So a single javascript server my entire purpose.
Things single javascript has all my javascript,jquery and other js files.
I am not sure if the idea to roll all javascripts in a ball has any flaws as such but I certainly need not worry about jquery not being loaded.
In the beginning of the article you mentioned reaching out to various people to determine what areas were experiencing an outage. I just thought I'd mention that there's a great tool that allows you to test from different locations all over the world. It's at http://www.webpagetest.org/ Currently they have 10 locations in the US.
@Sethu - A CDN is normally a pretty reliable thing. And the benefit comes in the form of reduced latency. The payoff is bigger when you have a global audience. Lastly, when it comes to jQuery the best option is to use Google's hosted version. The reason is that so many sites reference Google's version that the user is likely to have it cached in their browser before they ever reach your site. That's something I cover in my article here... http://blog.bucketsoft.com/2012/03/maximize-your-chances-of-caching-your.html
I wrote .NET code utilizing the Timer class to check the uptime of an external source by polling it every 2 minutes. By using the Timer, this code will execute on a separate background thread and will never slow down an ASP.NET request. So whenever the external source is down then I set a flag (a static variable) which when false will disable the script, or in this case failover to an internal version.
Learn't a new thing, will be including it in my best practices. :)
I confuse why ScriptManager control automatically generates script does not use protocol-less URLS.
jquery-rails-google-cdn and jquery-ui-rails-google-cdn.
I work at ISP in Russia, and our customers have similar problems with ajax.aspnetcdn.com.
Can you tell me the contacts of Microsoft CDN team?
Thanks in advance for your help!
Thanks for the great info!
Some info here : Pageload optimization using jQuery CDN http://markupjavascript.blogspot.com/2013/11/pageload-optimization-using-jquery-cdn.html
Comments are closed.