Thoughts from the team at Mission Data

Handling Session Timeouts (and other errors) using Ajax

Ajax can bring a much more responsive and intuitive feel to web applications. However, many times developers overlook error cases when using Ajax. What if the request fails? In one particular case a user’s session may have timed out before they made an Ajax call. This post describes one such way of handling this in a somewhat friendly way.

Handling session timeouts on non-Ajax requests

The first step to handing session timeouts with AJAX is to handle them for non-Ajax requests first. One way of doing this is trapping the URL the user is attempting to access (but not authenticated to), redirecting them to a login page, and upon success, redirecting them back to their original request.

If they aren’t authenticated, send them to the login page:

if(!loggedIn())
{
  response.sendRedirect("/login.jsp?uri=" + URLEncoder.encode(fullURI, "UTF8"));
}

Hide the original request in the page:

">
  ID:

  Password:



…and after a successful signup, send them back to where they wanted to go:

if(authenticated())
{
  response.sendRedirect(request.getParameter("uri"));
}

Handling Session Timeouts for Ajax Requests

The first thing we need to know is if it is an ajax request or not. Since I am going to use prototype for all my ajax calls it is as straight forward as looking for the special header prototype sends on each request:

private boolean isXMLHttpRequest(HttpServletRequest request)
{
  return request.getHeader("x-requested-with") != null && request.getHeader("x-requested-with").equals("XMLHttpRequest");
}

Once we know the request type, we will send a non-200 HTTP status code if their is an authentication error. From the standards, the most appropriate status code is a 401 (Unauthorized). This code, however, comes with some baggage. On a non-ajax HTTP request all browsers will respond by displaying an authentication dialog. Most browsers don’t do this on ajax calls. Most. Safari decided that it should pop up the authentication dialog on ajax requests. Without going too deeply into how this is probably correct and we should be using HTTP to its full advantage and using HTTP authentication instead of a custom authentication scheme, I don’t want this behavior. I landed on using the much less appropriate 403 (Forbidden).

So, if the user isn’t authenticated (such as with an expired session), send the 403 response back to the browser:

if(isXMLHttpRequest(request))
{
  response.sendError(403);
}

Now we handle the 403 on the browser side of things:

new Ajax.Request('/search',
     {asynchronous:true,
       evalScripts:true,
       onException: function(req,exception) {
         alert('An exception "' + exception + '" was thrown accessing "' + request.url + '"' )
         return true
       },
       on403: function(t) {
         alert('Your session appears to have expired.  You will asked to log in again and returned here.')
         window.location.reload()
         return true
       },
       onFailure: function(request) {
         alert('An unhandled error occured ' + t.status + ': ' + t.statusText);
         return true
       },
       onLoading:function(request){Element.show('loading')},
       onComplete:function(request){Element.hide('loading')},
       onSuccess: function(t) {
         //do something with the results
       },
       parameters:Form.serialize(this)
     })

The interesting bit of this call is the on403 method. Prototype will call onXXX() where XXX is a non-200 HTTP response code. When a 403 is returned, I simply reload the current page. Since we handled the scenario of authenticating and returning to the requested page for the non-Ajax call, this ‘just works’. You may wish to do something else here, including some nifty ajax widget that authenticates in-line and makes the call again.

Now that we have an Ajax request that suits our needs, we will want to use it throughout our project. Instead of repeating the same code over and over again, let’s DRY it up.

Creating a Wrapper Class

Since I am going to use this scheme for Ajax.Request and Ajax.Updater I created two wrapper classes that use a common function for setting the options. This scheme allows you to override any of the default options I use in the wrapper:

Ajax.MyRequest = Class.create(Ajax.Request, {
  initialize: function($super, url, options) {
    $super(url,_options(options));
  }
})

Ajax.MyUpdater = Class.create(Ajax.Updater, {
  initialize: function($super, container, url, options) {
    $super({success:container},url,_options(options));
  }
})

function _options(options) {
  return Object.extend({
     asynchronous:true,
     evalScripts:true,
     onException: function(request,exception) {
       Element.hide('loading')
       alert('An exception "' + exception + '" was thrown accessing "' + request.url + '"' )
       return true
     },
     on403: function(t) {
       Element.hide('loading')
       alert('Your session appears to have expired.  You will asked to log in again and returned here.')
       window.location.reload()
       return true
     },
     onFailure: function(t) {
       Element.hide('loading')
       alert('An unhandled error occured ' + t.status + ': ' + t.statusText);
     },
     onLoading:function(request){Element.show('loading')},
     onComplete:function(request){window.setTimeout('Element.hide("loading");', 100);}
  }, options || {});
}

One Response to “Handling Session Timeouts (and other errors) using Ajax”

  1. May 6th, 2009 @ 11:48 am jb responded:

    Nice job putting these pieces together. I was stuck trying to figure out how to handle expired sessions in my Ajax calls, and this method fits right in with my existing error-handling system. Thanks!

blog comments powered by Disqus