Education is an admirable thing, but it is well to remember from time to time that nothing that is worth
knowing can be taught. (Oscar Wilde)

Some irrelevant musings about Delphi programming and my life.


Subscribe in a reader

March 04, 2007

Ending a Server Session when a Browser Window closes

The infamous "Log Off" problem
I'm working on a webapplication using Delphi 2006 and Intraweb 8. Basic stuff: the user logs on, does some database work, and then logs off. Or rather: that's how I would like it to work...

The reality is, alas, that users rarely use the provided "log off" button, but close the browser window instead. And that poses a bit of a problem: the server doesn't know the session has ended, and can't release its resources until some preconfigured time-out has occurred (half an hour in my case). Wouldn't it be great if the server could detect the closing of the browser and not keep those valuable resources tied up for an unnecessary amount of time? Well, it turns out it can, sort of.

Searching the net I found a number of solutions to the "Log Off" problem. All solutions use the unload event of the body tag. The problem is that the unload event not only fires when the browser closes, but also when a user navigates to a different page. Although some clever "hacks" exist to distinguish between closing and navigating, I haven't found one that works reliably on IE and Firefox.


An alternative solution: Getting help from the Server
Given the fact that it's a bit problematic, from within the browser itself, to differentiate between the closing of a browser window and navigating to another page, couldn't we somehow get the server to help?

The idea is simple enough:
  • In the browser "unload" event we tell the server to set the session time-out to one minute.


  • When the server receives a page request we reset the session time-out to its default value
This will ensure that the session time-out remains set to its default value as long as the user keeps visiting pages on our server: an "unload" event (setting the time-out to one minute) will be followed by a page request (resetting the session time-out to its default value). However, when the user closes the browser window, or navigates to some other site, the session will time-out in one minute. In these cases the "unload" event is not followed by a page request.

Now all we need is a way to call the server from within the browser unload event. Obviously some asynchronous javascript and xml could come in handy (yep, you've got it: AJAX). Well, enough talking already, let's get down to some coding.


Implementing the Browser Close Detection
The implementation I'm giving is for Delphi 2006 and Intraweb 8, but the same solution could also be implemented for other versions or web frameworks.

First let's examine the javascript that's needed to call the server from within the browser unload event. It's fairly straightforward. First we create a xmlhttp request object, and then we call the special url "endsession" on the server providing the intraweb sessionid "GAppID" as the post data. We use a POST rather than a GET command because Internet Explorer caches GET commands.

function unload(){
  if(window.XMLHttpRequest){
    var xmlhttp = new XMLHttpRequest();
  }else{
    var xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
  };
  if(xmlhttp != null){
    data = "GAppID="+GAppID;
    url = GURLBase+"/endsession";
    xmlhttp.open("POST", url, false);
    xmlhttp.send(data);
  };
}


The javascript must be part of all the pages of our webapplication. A simple way of achieving this in Intraweb is to use a base form, and derive all our other pages from this base form. In the constructor code of the base form, we add the javascript, and an initialization command that links the script to the beforeunload event of the page. We're using the "onbeforeunload" event instead of the "onunload" event because we can't be sure the "onunload" event will give us enough time to fullfil the server request before the browser closes. In contrast, the "onbeforeunload" event will actually wait for the result of our server request before continuing.


constructor TFRMIWBase.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  // add javascript to the form (body left out for brevity)
  Javascript.Add('function unload(){');
  javascript.Add(<rest of body>);
  Javascript.Add('}');
  // link the javascript to the onbeforeunload browser event
  AddToInitProc('window.onbeforeunload = unload;');
end;


On the server side, we have to intercept the "endsession" url, and set the session time-out to 1 minute. In Intraweb we can achieve this by catching the url in the OnBeforeDispatch event of the servercontroller:

procedure TIWServerController.IWServerControllerBaseBeforeDispatch(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  Session: TIWApplication;
begin
  if Pos('/endsession', Lowercase(Request.PathInfo)) > 0 then
  begin
    Session := GSessions.LookupAndLock(Request.ContentFields.Values['GAppID']);
    if Assigned(Session) then
    begin
      try
        Session.SessionTimeOut := 1;
        Response.StatusCode := 200;
        Response.ContentType := 'text/xml';
        Response.Content := '<xml/>';
        Response.SendResponse;
        Handled := true;
      finally
        Session.Unlock;
      end;
    end;
  end;
end;


As expected, the "endsession" url will be called when the browser window is closed, but also when the user navigates to a new page. So it's important to reset the session time-out to its default value when a page is requested. In Intraweb we can use the OnAfterRender() event of the servercontroller:


procedure TIWServerController.IWServerControllerBaseAfterRender(ASession: TIWApplication; AForm: TIWBaseForm);
begin
  // set the current session timeout to the default servercontroller timeout
  ASession.SessionTimeOut := SessionTimeout;
end;


7 comments:

MadMaxRedux said...

Thanks for posting the tip. I will give it a try with Delphi 7 and IW8

Anonymous said...

Works in the Standalone server for D2006 and IW8; does not work in the
ISAPI dll with IIS5. If you have any ideas why, i would appreciate it. thanks. John j*ohn@jcb-ell.com
(remove * and -)

Robert Cram said...

You are right. The code needs two small changes to make it work as an isapi under IIS.

1. In the javascript function
replace: url="endsession"
with: url=GURLBase+"/endsession"

2. In the beforedispatch handler
replace: Request.QueryFields.Value['GAppID']
with: Request.ContentFields.Value['GAppID']

I've updated the blog with these changes.

burndata said...

excellent article. I was happy to integrate it into my IW app. I didn't make a base form to feed off of like your article suggested...

I also changed some other goodies...

Lenn Dolling
WhiteCrow.ca

OMOHWOVO SUNNY said...

Your suggestions are quite useful, but how possible is it to create open multiple windows in an Intraweb Session without closing the calling window? thanks.

Anonymous said...

Great idea! I tried it with Delphi 7 and IW 9. Very useful!

Anonymous said...

MANY THANKS! I use this with Delphi 2009 / IW10 and it works fine :)

Interesting Links