A sensible approach to XHR timeouts

Updated: 18/01/2013 : I have implemented a more complete version of these timeouts in an XHR JavaScript API here:  http://code.google.com/p/xhr-progressive-timeouts/

I am in the process of developing an html5 application that will run on a mobile device over 3g or even over E (as is the case during my testing due to poor coverage in-doors). The ajax requests the client is making may return a significant amount of data. At the point of making the request it just doesn’t know how much data will be being sent.

Testing over E gave an unreliable network connection (which is a good thing, as it allowed me to harden the network code of both my client and server code).

Part of the problem I had was when the network would go down, the synchronisation process would sit there for a very long time waiting for a particularly large timeout to expire. A large timeout was required because the client could be receiving very large amounts of data (megabytes) over a particularly slow connection.

I needed a more sensible timeout scheme. Initially I thought, being able to set different timeouts at different readyStates of the ajax request would be a good idea, so for example, the client would wait a short time for the request to initialise (go from state 0 to state 1) then a larger value for it to connect (state 1 to 2) then once it started receiving data, ramp the timeout up significantly to allow for large amounts of data.

The problem with this approach was that if the network was going to fail, it would invariably fail during readyState 3 where most of the network traffic was occurring. Unfortunately this meant the client was already using a very large timeout, so it would be along time before it knew there was a network issue, leaving the little sync icon pulsating for ages.

I needed an even better timeout scheme. One option would be to determine the amount of data that was going to be sent to the client and calculate a sensible timeout based on the size of data. Problem with this is that the timeout should probably be different depending on the speed of the connection.

What I really wanted to achieve was the ajax request taking as much time as it needed to receive the data, but should the data stop flowing, timeout within a relatively short amount of time. The solution was so simple and very elegant and achieves exactly what I wanted.

The principle of the solution is to keep the timeout a set number of seconds ahead of the time already taken, sort of like a timeout buffer. The size of the buffer being different for different stages of the request. A short timeout when initialising, longer timeout for connecting, and likewise while receiving data.

var timeouts = [ 5000, 10000, 20000, 20000, 0 ]; // readyState 0 - 4 timeouts 

I am using dojo.xhr which has a built in timeout mechanism, I simply set the timeout required on the request object. If your using raw ajax you should set the timeout property of the XHR object.

request.timeout = timeouts[0]; // initial timeout, readyState 0 

I also record the start time of the request

request.started = new Date(); 

I then need to watch the readyState changes on this request, to do this I intercept the onreadystatechanged event of the XHR object. Again as I am using dojo.xhr and it returns a deferred object, I need to poke inside that object to get hold of actual XHR object

(function(deferred){
  var io = deferred.ioArgs, xhr = io.xhr, handler;
})(deferred); 

I then need to create the function that will watch the readyState changed events, and hook into the event. The call to ready() just ensures that the timeout is initialised for the current readyState.

(function(deferred){
  var io = deferred.ioArgs, xhr = io.xhr, handler, ready = function() { };
  ready();
  handler = xhr.onreadystatechange;
  xhr.onreadystatechange = ready;
  xhr.ontimeout = function() { /* handle timeout here */ }
})(deferred); 

Next we need to work out the new timeout to set at each ready state. To do this I calculate the time taken so far, and the timeout buffer we want for the current XHR readyState. Then I set the new timeout as the time take so far + the timeout buffer. Lastly, we call the original readyState change hander. Note: for readyState 4 the timeout is 0, so I set timeout to 0 also, essentially disabling the timeout once we reach ready state 4.

(function(deferred, timeouts){
  var io = deferred.ioArgs, xhr = io.xhr, handler, ready = function() {
    var sofar = (new Date()).valueOf() - request.started.valueOf(),
      timeout = timeouts[xhr.readyState];
    io.args.timeout = timeout ? sofar + timeout : 0;
    if (handler) handler.apply(this,arguments);
  };
  ready();
  handler = xhr.onreadystatechange;
  xhr.onreadystatechange = ready;
  xhr.ontimeout = function() { /* handle timeout here */ }
})(deferred, timeouts); 

And that’s it. The timeout on the connection will always be 20 seconds from now (when receiving data) and the request can take as long as it needs to receive all the data.

Here is an example using plain XHR (requires XMLHttpRequest level 2).  Not all browsers support the timeout attribute yet.  It can be emulated using a timer or by watching in-flight requests and checking their timeout values.

function ajax(url, onready) {
  var started = new Date(),
      timeouts = [ 5000, 10000, 20000, 20000, 0 ],
      xhr = new XMLHttpRequest();
  xhr.timeout = timeouts[0];
  xhr.onreadystatechange = function() {
  var sofar = (new Date()).valueOf() - started.valueOf(),
  timeout = timeouts[this.readyState];
  xhr.timeout = timeout ? sofar + timeout : 0;
  if (xhr.readyState == 4 && xhr.statue == 200 && onready) {
    onready.apply(this);
  }
 };
 xhr.open("get", url, true);
 xhr.send();
}

// request data using ajax with cool timeouts
ajax('/get', function() {
  if (this.responseText) {
    // ....
  }
});
 

One Response to A sensible approach to XHR timeouts

  1. Pingback: A sensible approach to xhr timeouts « Austin France's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s