Tuesday, December 15, 2009

Detecting download failures with script tags

Matt Mastracci has done some experimentation and found that most browsers provide some callback or another for indicating that a script tag has failed to download. This is very interesting, because script tags don't have to follow the Same Origin Policy. Here is my replication, for the possible aid of anyone else wandering in these murky woods.

Available callbacks

The simplest callback is the onerror attribute. It can be attached to a script tag like this:

script.onerror = function() {
/* code here is called if the download fails */
}


For completeness, there is also an onload attribute. It's analagous to onerrer except that it indicates success rather than failure. It can be attached to a script tag like this:

script.onload = function() {
/* code here is called if the download succeeded */
}


Finally, IE supports onreadystatechange, similarly to the XHR attribute of the same name. The supplied callback will be invoked as the download progresses. The state of the download can be queried via the readyState attribute, which will reach state 'loaded' and/or 'complete'.

script.onreadystatechange= function () {
if (script.readyState == 'loaded') {
script.onreadystatechange = function () { } // prevent duplicate calls
/* error handling code goes here */
}
}


Results

I used the test page to see which of the three events are fired on several browsers.

Loading a bad page:
Firefox 3.5: onerror
Safari 4: onerror
Chrome 4: onerror
IE 7: onreadystatechange
IE 8: onreadystatechange

Loading a good page:
Firefox 3.5: onload
Safari 4: onload
Chrome 4: onload
IE 7: onreadystatechange (if not cached)
IE 8: onreadystatechange (if not cached)


Analysis

The onerror attribute works on all browsers but IE. For IE, onreadystatechange is available. Conveniently, no browser supports both of them, so a handler hooked up to both of them will fire exactly once.

A complication on IE is that onreadystatechange doesn't differentiate whether the download succeeded or not. Downloading a non-cached version looks the same as a download failure. Thus, any code using onreadystatechange needs to check whether the download succeeded or not.

Followup: Order of evaluation versus onreadystatechange

On IE, if onreadystatechange indicates the installation is complete, in what circumstances should the loading be considered to have failed?

I did a followup test where the loaded code (exists.js) does a window.alert. That way, I can see which happens first: the alert, or the onreadystatechange callback. On both IE7 and IE8, the alert happens first. That means if the script sets a global flag to true once it loads, the onreadystatechange callback can check it and reliably determine whether the download has succeeded.


Test script

<head>
<title>Test page</title>

<script>
function loadFile(url) {
var head = document.getElementsByTagName('head').item(0);
var script = document.createElement('script');
script.src = url;
script.onload = function() {
window.alert("onload called");
}
script.onerror = function() {
window.alert("onerror called");
}
script.onreadystatechange= function () {
if (script.readyState == 'loaded') {
script.onreadystatechange = function () { }
window.alert("onreadystatechange (" + script.readyState + ")");
}
}
head.appendChild(script);

}

function good() {
loadFile("exists.js");
}
function bad() {
loadFile("bad.js");
}
</script>
</head>

<body>
<input type="button" value="good" onclick="good()">
<input type="button" value="bad" onclick="bad()">
</body>

No comments: