jueves 2 de octubre de 2008

Http Sub-Sessions for different browser tabs (in IWebMvc2)

I'm not sure who to blame here, the browsers or the HTTP protocol, but it's pretty clear that one of the worst limitations web apps suffer today is the fact that all the windows/tabs a user opens (without spawning a new process) share the same server session. This has been regarded as an unsolvable problem for ages and it won't be really solved until the browsers start sending a new HTTP-Header with the window ID and it's handled accordingly.

Sometime ago I learned that Wicket was able to overcome this problem. I tried a peek to the code (wicket.markup.html.WebPage if you're interested) but it was so entangled with the components that it was not clear how it was doing it at all. But it gave me the basic idea, they were tinkering with the javascript Window object. Of course! Each tab has a different Window instance and hence a unique window.name.

With that info the steps to follow were simple so I started an implementation for IWebMvc2. The first task is to assign the container window a name. Pretty easy:

window.name = "session_${requestScope.session_id}"

The name will then be attached to all the requests (GET/POST/AJAX). For HTTP POST we need a simple input hidden (it can be filled during onSubmit, for example). For DWR a global parameters object can be used (see the doc). Links are more problematic though. Basically because the user can right click and Open in New Window/Tab rendering our efforts useless. We can dodge the problem attaching the window name just to left clicks. Dojo helps here:

dojo.query("a").onclick(function(e) {
   href = e.target.href
   // attach session_id to query string here
})

And with that we have finished the client code! The server needs to intercept each request (before any other service starts working with it!) and create sessions when needed. In summary, it will create a new session when no Window name is received and/or when the identifier received has been already used (yes, the session identifier includes a unique string and a number tracking the usage). In Java this kind of functionality is performed using a Filter. This filter needs to detect the tab:

String id = httpRequest.getParameter(SUB_SESSION_ID);
long usage = 0;
if (hasText(id)) {
   String[] parts = id.split("_");
   id = parts[0];
   usage = Long.parseLong(parts[1]);
}

Obtain the sub-session

HttpSession session = httpRequest.getSession(true);
SubSession subsession = (SubSession) session.getAttribute(id);
subsession.incrementUsage();

Generate one if none is available (for the provided identifier):

if (subsession == null)
   subsession = generateSubSession(session);
else if (usage < subsession.getUsage())
   subsession = generateSubSession(session);

And finally assign the subsession as the current session for the rest of the request. All the successive components will see it as the actual session (for example, JSTL). Easy :-)

Request req = new SubSessionServletRequest(httpRequest, subsession);
super.doFilter(req, response, filterChain);

That's all, really (code has been committed to the repository). I've created a little screencast (a much cleaner version here):

video

And now the sad news! There's a known limitation. If the user hits reload it will obtain a new sub-session because either the window name is not sent or the name sent is outdated. I'm working on it! Ideas are welcome. On the bright side, CRTL+N/CRTL+T works fine (even with cut&paste).

<UPDATE>While working to solve the Refresh problem I started looking to the onBeforeUnload event. And it resulted in a very clean way to detect new windows! The event is just fired when the current document is going to be destroyed but not for new tabs generated by the user or links/form posts with new targets. This means that when the onBeforeUnload event is fired the user is working inside the same session. That simplifies the client code a lot and in turn the server side as well.

My final solution (pending commit yet) uses a temporal Cookie to inform the server of the session in use instead of the hidden field/query parameter. Just because it's a little bit more homogeneous. It works for GET/POST/AJAX in the same way, as cookies are always included in the HTTP request (but the old solution, explained above, can be modified with some work to use the event approach). The server side is concise, if a session cookie is received then uses it otherwise it generates one.</UPDATE>

By the way, I haven't yet considered security (I haven't added it to IWebMvc2) but my guess is I will probably be able to let the user have different credentials for each tab if so is desired.