The issue of how to propagate some errors in our Django web app

August 3, 2020

Much of what our Django application to handle (Unix) account requests does is only available to special people such as professors, who can actually approve account requests instead of just making them. Following our usual, we protect the management section of the web app with Apache HTTP Basic Authentication, where only people in designated Unix groups (such as the 'sponsors' group) have access. However, the Django application also has a 'users' table, and in order to have access to the application you have to be in it (as well as be in a Unix group that's allowed access). Normally we maintain these two things in sync; when we add someone to the relevant Unix group, we also add them as a user (and set the type of user they are, and set up other necessary data for people who can sponsor accounts). But sometimes we overlook this extra step so people wind up permitted by Apache but not in the 'users' table. If they actually try to use our web application, this causes it to stop with an error.

(This 'users' table is part of the application's own set of tables and models, not the normal Django authentication system's User table. Possibly I should have handled this differently so that it's more integrated with normal Django stuff, but when I started this web application I was new to Django and keeping things completely separate was much easier.)

Right now, a bunch of our views look like this at the start:

def approve(request):
  urec = get_user(request)
  if urec.is_staff():

You'll note that there's no error handling. This is because get_user() does the brute force simple thing (with some error checks removed):

def get_user(request):
  user = request.META['REMOTE_USER']
    return models.User.objects.get(login=user)
  except models.User.DoesNotExist:
    ... log a message ...
    raise django.http.Http404

This is simple and reliable, but it has a downside, which is that people who run into this mistake of ours get the same HTTP 404 error page that they'd get if they were trying to go to a URL that genuinely didn't exist in our application. This is at best uninformative and at worst confusing, and I'd like to do better. Unfortunately I'm not sure what the best way to do it is.

My first attempt was to raise Django's Http404 error with a specific message string and then try to make our template for the application's 404 error page check for that string and generate a different set of messages. That failed, because as far as I can see either Django drops the message string at some point in its processing or doesn't pass it to your custom template as a template variable.

I can see three alternate approaches, none of which I'm persuaded by. The simple but unappealing option is to change get_user() to return an error in this situation. This would require a boilerplate change at every place it's called to check the error and handle it by generating a standard 'we screwed up' response page, which makes the code feel like Go instead of Python. But at least how things worked would be obvious (and if I returned None, I could make failures to handle this case relatively obvious).

The more complicated but less code approach is to raise a custom error and wrap every function that calls get_user() with a decorator that catches the error to generate and return the standard explanation page. I would have to decorate every view function that directly or indirectly calls get_user() (and remember to add this if I added new functions), and decorators are sort of advanced Python magic that aren't necessarily either clear or straightforward for people to follow.

I suspect that the way I'm supposed to do this in Django is with some form of middleware. If I kept to much of the current approach, I could do this with a middleware that just used process_exception(), but that doesn't seem the most idiomatic way. Since this is a common processing step for anything protected behind HTTP Basic Authentication, it feels like the middleware should do the user lookup itself and attach it to the request somehow, with the actual views not even calling get_user(). But I don't know how to attach arbitrary data to Django's request objects, and anyway that involves even more Django magic than middleware that catches a custom exception (and the magic is less clear, since the view functions would just access data without any obvious reason for it to be there).

(I care about the amount of magic involved in any solution because my co-workers aren't particularly familiar with Django and even I only touch the code every once in a while. Possibly this means I should just use the explicit error checking version, even if it makes me twitch.)

Written on 03 August 2020.
« Getting my head around the choice between sleeping and 'tickers'
Exim's change to 'taint' some Exim variables is going to cause us pain »

Page tools: View Source, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Mon Aug 3 00:16:30 2020
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.