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?
Session timeout has been a very common feature in Ajax-based web applications.
The default value is 0. Which means there is no timeout.
Yes it's safe. As far as load, that's up to your hardware and how you write it, but it has no worse effect than users refreshing the page (arguably less considering the overhead of an AJAX call over a standard page load).
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.
You could use something like http://amplifyjs.com/ that lets you write a nice wrapper for your AJAX calls and then use its data mapping feature to check if the user is still logged in before doing the AJAX call.
This way you can have a client-side timer that sets the user to logged-out status and provides a hint so the login check doesn't need to be done before every AJAX call.
Alternatively you can use a custom decoder which asks the user to log in and retries the AJAX call if the user was logged out. It would need to store all the xhr data and callbacks it gets called with until the user logs in.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With