What is the best way to handle session timeouts in ajax queries?

advertisements

Consider this Django view which will get a list of items associated to the current user:

@login_required
def list_items(request, page_number=0):
    items = Paginator(request.user.items, 5).page(page_number).object_list
    return HttpResponse(cjson.encode(items))

Obviously, it wants to use the login_required decorator, to restrict access to the view for logged-in users.

What does login_required do when a non-authenticated user tries to access the view? It returns a HttpResponseRedirect toward settings.LOGIN_URL.

Consider this JavaScript code, which calls the view:

var getPage = function(pageNumber) {
    $.ajax({
        url: "/list_items/" + pageNumber + "/",
        success: function(data) {
            $("#list_container").html(formatData(data))
        }
    });
};

Suppose settings.SESSION_COOKIE_AGE = 60 seconds.

If a user goes to page 1, reads it for 61 seconds, then clicks on the button for page 2, Django's login_required decorator will detect that the session is no longer active, and will return a HttpResponseRedirect(settings.LOGIN_URL), which will cause the success callback to get a HTML login page instead of the JSON-encoded list.

This is where it happens.
It's called by user_passes_test here.

What's the best way to handle this?

Here's a few things I've thought of:

1. The success callback should check the response, and see if it gets a login page, by whatever means (check if content-type is html, check contents, etc). But this means that we have to wrap all AJAX calls with a callback wrapper like so:

    $.ajax({
        url: "/list_items/" + pageNumber + "/",
        success: sessionExpiryCallbackWrapper(function(data) {
            $("#list_container").html(formatData(data))
        })
    });

But this is ugly, and developers might forget to do this everywhere.

2. Use $.ajaxComplete to handle all requests.

    $.ajaxComplete(globalCompleteCallback);
    $.ajax({
        success: successCallback,
        complete: completeCallback
    });

But this is the call order:

    successCallback(); // success is called before complete
    completeCallback();
    globalCompleteCallback(); // this is called after the local callback

So we only catch the redirect, after successCallback has failed, and possibly with JS errors due to the invalid data it received.

3. If login_required would return 403 on AJAX requests:

    if not user.is_authenticated():
        if request.is_ajax():
            # send 403 to ajax calls
            return HttpResponse403("you are not logged in")
        else:
            # regular code path
            return HttpResponseRedirect(settings.LOGIN_URL)

But login_required just uses user_passes_test which doesn't do this.

user_passes_test has a lot of functionality in there, so it's not such a good idea to reimplement it.

What's the best way to handle the timeouts for AJAX calls?


I would handle it by having your session timeout method check whether or not it is being requested with AJAX. If it is ajax, return a 401 not authorized(or 403 forbidden or whatever status makes sense) status code with an empty json string. Next, in your javascript, bind a global ajaxError handler that checks for that status code and handles it appropriately.