Fallback for CDN Provided JS When Using CSP

How to provide local js library fallback when inline scripts are disallowed

Posted by Stefán Orri Stefánsson on 18 October 2015

The solution, according to the Internet


<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<script>window.jQuery || document.write('<script src="path/to/your/jquery"><\/script>')</script>

Short and sweet, and totally fails for my site which is using CSP (without “unsafe-inline”) and therefore disallows inline scripts. How about moving it to a separate js file - the main app.js file perhaps?

Moving the fallback script loading to the main app.js file


if (window.jQuery) {
	InitializeApp(); //My main entry point, sets up events and stuff
} else {
	// jQuery hasn't loaded. Load local copy by adding script tag to header
	var script = document.createElement('script');
	script.src = 'js/jquery_1.11.3.min.js';
	script.async = false;

	// Then bind the event to the callback function.
	var callback = InitializeApp;
	script.onreadystatechange = callback;
	script.onload = callback;

	// Fire the loading
	document.head.appendChild(script);
}

Still relatively simple, and gets the job done. Or does it? App.js now works as intended, but another problem surfaced. The next script loaded after app.js is bootstrap.min.js which depends on jQuery. This now throws an error because it loads and executes straight after app.js, and at this point the jQuery fallback hasn’t been loaded. There is really no way around this using this approach, so back to the drawing board it is.

Require.js?

At this point you might ask why not use a js dependency management library like require.js. Easy CDN fallback is indeed one of its benefits. I didn’t really need dependency management for my tiny site but the real dealbreaker was me using subresource integrity (SRI). Require.js supports SRI since version 2.1.19, but I wanted to have the original CDN script tag, with the SRI attribute, right in the HTML for demonstrative purposes.

The final solution

Remove all script tags except CDN jQuery and a new one, script_loader.js. This script loads all the other scripts, including the local copy of jQuery if needed.


function() {
	var scripts = [
		'js/bootstrap.min.js',
		'js/handlebars.runtime-v3.0.0.min.js',
		'js/app.js'
	]
	if (!window.jQuery) {
		//Insert local jQuery at the front of the array
		scripts.unshift('js/jquery.min.js');
	}
	scripts.forEach(function(src) {
		var script = document.createElement('script');
		script.src = src;
		script.async = false;
		document.head.appendChild(script);
	});
}();

Now my HTML looks like this at the very end.


	<script src="https://code.jquery.com/jquery-1.11.3.min.js" integrity="sha384-6ePHh72Rl3hKio4HiJ841psfsRJveeS+aLoaEf3BWfS+gTF0XdAqku2ka8VddikM" crossorigin="anonymous"></script>
	<script src="js/script_loader.js"></script>
	</body>
</html>

Photo credit: Grænavatn, Veiðivötn, Iceland. Own photo.