Posted in jQuery, ColdFusion | Posted on 09-08-2010 | 4,894 views
Earlier today a user on cf-talk asked about handling session time outs in an Ajax based application. I'm happy he asked this because I'm been worrying about this myself for a while now. One of my applications, Lighthouse Pro, makes heavy use of Ajax. The main issue display is entirely Ajax driven. If your session times out though you get... nothing. That sucks. So I used this as an excuse to get off my butt and fix it once and for all. I also whipped up a quick simple example that I'll use for the blog entry. I'd love to get people's opinions on it and start a discussion about how others have solved this issue as well. (I'll also say that there will be a follow up later talking about handling ColdFusion errors in general.)
To begin, let's create an incredibly simple application. First, our Application.cfc with a super simple security system.
2 this.name="ajaxtimeout";
3 this.sessionManagement="true";
4 this.sessiontimeout=createTimeSpan(0,0,0,31);
5
6 public boolean function onRequestStart(string req) {
7 if(structKeyExists(form, "login")) {
8 session.loggedin = true;
9 }
10
11 if(!structKeyExists(session, "loggedin") && listlast(arguments.req,"/") != "login.cfm") {
12 location(url="login.cfm");
13 }
14 return true;
15 }
16
17
18}
As you can see, we use the onRequestStart method to look for a session variable. If it doesn't exist, we push the user to the login page. If we see the login key in the form scope, we mark the user as logged in. (This is normally where you would actually check the username and password.) Now let's look at our Ajax "application."
2
3<head>
4<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
5<script>
6$(document).ready(function() {
7
8 $("#testLink").click(function(e) {
9 $("#showResult").load("rand.cfm");
10 e.preventDefault();
11 });
12})
13</script>
14</head>
15
16<body>
17
18
19Click the link to perform an Ajax request to load a random number: <a href="" id="testLink">Click Me, Baby</a>
20
21<p>
22
23<div id="showResult"></div>
24
25</body>
26</html>
Woot. That's no GMail for sure. The so-called application has one link. When you click on it I'm going to load the contents of rand.cfm into the div. rand.cfm simply outputs a random number so I won't bother showing the code for that. So you may have noticed that my session timeout was extremely small. If you run this code as is, login (you can enter anything or nothing), and then let the app site for a bit over half a minute, you will see that clicking the link returns the login form to the user. In a more complex Ajax app where JSON was requested, the login form wouldn't be valid JSON so nothing at all would happen. (As with Lighthouse Pro.) Here is an example:

So, let's talk about how we can handle this. Currently our security logic is that if you are not logged in, we push you to a new CFM. That works fine for normal requests, but for an Ajax request we should handle it differently. We could simply output a string, like "SessionTimeout", but then we would need to update our jQuery code to look for that particular string. Instead, why not simply throw an error that we can listen for? Let's look at an example of that first:
2 if(structKeyExists(form, "login")) {
3 session.loggedin = true;
4 }
5
6 if(!structKeyExists(session, "loggedin") && listlast(arguments.req,"/") != "login.cfm") {
7 //for ajax requests, throw an error
8 var reqData = getHTTPRequestData();
9 if(structKeyExists(reqData.headers,"X-Requested-With") && reqData.headers["X-Requested-With"] eq "XMLHttpRequest") throw(message="SessionTimeout");
10 else location(url="login.cfm");
11 }
12 return true;
13}
I've modified onRequestStart to look for a Ajax request. This is done by looking at the HTTP Request Data. It isn't perfect but should work well for 99.99% of your clients. If the request is an Ajax one, I'm throwing an exception with a particular message. Otherwise we simply do the default location.
Luckily, jQuery provides an excellent way to handle errors across all Ajax requests in a page. The ajaxSetup method can be used to handle errors. Mahesh Chari has a great blog post on the topic: How to handle ajax errors using jQuery ? I used his example, modified, to look for a particular type of error we are going to work with in a bit.
2$(document).ready(function() {
3
4 $.ajaxSetup({
5 error:function(x,e){
6 if(x.status == 500 && x.statusText == "SessionTimeout") {
7 alert("Your session has timed out.");
8 location.href = 'login.cfm';
9 }
10 }
11 });
12
13 $("#testLink").click(function(e) {
14 $("#showResult").load("rand.cfm");
15 e.preventDefault();
16 });
17})
18</script>
I love this code for a few reasons. First, notice that my original code is not changed. It's logic is the exact same. Instead, I added an error handler at the page level. It then can worry about handling errors whereas my other function can stay simple. How you handle this error is obviously up to you, but I decided to use the alert function to let the user know about the issue and then I push them to the login. Obviously it could be done a bit snazzier.

Woot! Everything is perfect. Almost... You may be wondering - what happens if you have an error handler for your site? (Sorry - I said "if", everyone has an error handler for their site.) Well, that makes things... interesting. Consider the following onError:
2 writelog(file='application', text='my onerror ran');
3}
This is a fairly simple, and useless, error handler. Normally you would want to tell the user something, but for now, it at least hides the default ColdFusion errors from returning to the screen. But now we are intentionally throwing an error - so what do we do? We end up then needing to actually rethrow the error. CFScript doesn't support rethrow, so it is a bit more wordier in the script version, but here is what I used:
2 if(arguments.exception.rootcause.message == "SessionTimeout") throw(message=arguments.exception.rootcause.message);
3 writelog(file='application', text='my onerror ran: #serializejson(arguments.exception.rootcause.message)#');
4}
Basically - look at the exception - and if you see that it was my Ajax-specific timeout, recreate it and let it leak out to the response.
Comments? As a quick note - you may wonder - what about normal ColdFusion errors? How would you handle that in an Ajax-based application. I'm going to talk about that next.


* Log a timestamp of the last 'hit' by a given user in their session
* Create a simple facade that looks at the time difference between the last hit and the current time
* Create a ajax proxy to query that session facade
* Make sure the proxy request does not update the 'last hit' - otherwise your session will never time out
* Run a call to the session facade (via the ajax proxy) on a set interval
* Evaluate the inactivity period, if in a 'warning' period (say between 15-19 minutes) display a modal window warning the user.
* If the user chooses to extend the session, ping the session facade which will update the 'last hit' variable.
* If the inactivity is beyond the session timeout (I usually go with >= 19 minutes display a modal (non-closable) window telling them the session has timed out
(read more: http://cfsilence.com/blog/client/index.cfm/2008/4/...)
Only thing with your method is that you'd have to modify it a bit to work with Flex (which has the same potential problem really).
@Todd: Why would I have an issue with Flex? In Flex you can also use fault handlers for service calls. Could I not then just use them?
@Ray - the session timeout is not a security feature, it is a way to keep ram cleaned out..A logout routine that kills the session is a security feature. Relying on a session timeout for security is <ahem> lazy - sorry.
My job as a developer is to make the user experience super simple - let them focus on the task at hand - A user should not be expected to keep up with low level tasks like keeping their session alive. So, instead, you create a very small session timeout 3-5 minutes; and keep it alive --while they are on your site--. People get phone calls, called out into the front yard to deal with skinned knees, get interrupted by twitter or people wandering into their cube - and sometimes go to lunch. Their short attention span should not cause them to have to start the task over. Yes, it does have the potential to be bad, but so do asteroids hitting the earth. How likely is it going to happen? What is the risk? When you design your app you have to determine what is OK, what is not. In the case of LHP, keeping a session alive for a user all day is not a big deal - and that user will appreciate not having to log in every time they want to work a bug. For an ecomm app, it could be a bad idea. Sometimes we geeks need to step back and take a deep breath and remember that people use our apps, and there is no way we can protect them all; nor should we. Remember, nothing is foolproof to a sufficiently talented fool.
First off - no one is saying to rely _only_ on session timeout. It is a part of a whole package of things you can do. Be honest - user's don't logout. Some do - most won't. So having a basic "if you are idle for N you are gone" is a VERY good backup. How do you differentiate between a user going to lunch and a user who was at a coffee shop going home? You can't.
(I said "First Off", but I think that's it)
(No, I lie, I've got more.)
Ecom: Many ecommerce sites will store your cart in a permanent scope (like client). So while you may log out, your cart still has items. The important stuff like your name, CC info, etc, is expired safely, but not your items.
Shame on the user at the coffe shop going home..but, be honest, how many users at the coffe shop go home without at least closing thier browser. The days of internet cafe's with thier own 'puters are almost over; folks have ipads, netbooks, laptops and smart phones. but enough already; there is enough fodder here for folks to think about when planning an application.
About your post.. It is a good way to handle session timeouts for ajax calls where the whole page is a single ajax call. What would you do if your ajaxy stuff is in a widget, or dynamically changing a section of a form based on user input - and the user got interrupted. I think adding ajax into a website means you rethink how you "traditionally" managed a web app.
How is my solution only appropriate for a 'whole' page Ajax app? My demo is specifically _not_ that. It has content - and part of the page is loaded via Ajax.
BUT as i read over everything I realize my complaint is not how you catch the error, which is good, it is how you handle it..which is not the crux of the post, so I will just shut up now..
And i just noticed i still have an open issue on the copy on RIAforge ><
I also use ExtJS to provide an event system if people wish to explore that further.
http://www.andyscott.id.au/2010/8/27/How-to-handle...
I noticed this doesn't work unless "Enable Robust Exception Information " is check in the coldfusion admin. Is this normal to have this option checked for production servers (still assuming you are still hiding this extra info with a custom error)?
Here is the solution I used.
Within my Application.cfc, when a session timeout error occurs, I add a new header to the response. Unfortunately you can't use cfheader in cfscript yet so I have to use an include, which is kind of silly. But anyway, I do:
public void function onError(exception,eventname) {
if(arguments.exception.rootcause.message == "SessionTimeout") {
include "header.cfm";
throw(message=arguments.exception.rootcause.message);
}
writelog(file='application', text='my onerror ran: #serializejson(arguments.exception.rootcause.message)#');
}
My header.cfm is just:
<cfheader name="SessionTimeout" value="1">
Then I modified my JS to check for the header. I'd never done that before in JS but it was pretty trivial:
var sessionTimeout = x.getResponseHeader("SessionTimeout");
if(sessionTimeout == 1) {
alert("Your session has timed out.");
location.href = 'login.cfm';
}
I'll be posting this as a full entry later today, so any comments before then are definitely welcome.
I add a header to the response with cfheader and in the javascript, I used ajaxSetup and in the complete option i just checked for the custom header like this:
if(XMLHttpRequest.getResponseHeader('error') != null && XMLHttpRequest.getResponseHeader('error') == 'SessionTimeout')
[Add Comment] [Subscribe to Comments]