Note that the extension this talks about has subsequently been renamed to django_exceptional_middleware.
Django has support for emitting a response with any HTTP status code. However some of these are exceptional conditions, so the natural Pythonic way of generating them would be by throwing a suitable exception. However except for two very common situations (in which you get default template rendering unless you’re prepared to do a bit of work), Django doesn’t make this an easy approach.
At the third /dev/fort we needed something to make this easy, which Richard Boulton threw together and I subsequently turned into a Django app. It allows you to override the way that exceptional status codes are rendered.
Usual HTTP status codes in Django
In Django, HTTP responses are built up as an object, and one of the things you can set is the HTTP status code, a number that tells your user agent (web browser, indexing spider, Richard Stallman’s email gateway) what to do with the rest of the response. Django has some convenient built-in support for various common status codes.
The default is
200 OK, which usually means “here’s your web page, all is well” (or “here’s your image” or whatever). This is the code that you get automatically when a Django view function spits out a response, often by rendering a template.
Another thing you often want to do is to issue a redirect, most commonly
301 Moved Permanentlyor
302 Found. For instance, you do this if you have an HTML
POSTs some data, and then you want to direct the web browser (indexing spider, RMS’s gateway) to another page once you’ve done whatever updating action is represented by the
404 Not Found, which is usually the correct response when the URI doesn’t refer to anything within your system. Django handles this automatically if the URI doesn’t match anything in your Django project’s URLconf, and it’ll also emit it if you raise an
Http404exception in a view.
500 Internal Server Error, which Django uses if something goes wrong in your code, or say if your database is down and you haven’t catered for that.
For 200, your code is in control of the HTTP response entity, which is generally the thing displayed in a web browser, parsed by an indexing spider, or turning up in an email. For 301 and 302, the entity is ignored in many cases, although it can contain say a web page explaining the redirect.
For 404 and 500, the default Django behaviour is to render the templates
500.html respectively; you can change the function that does this, which allows you to set your own RenderContext appropriately (so that you can inject some variables common across all your pages, for instance). Usually I make this use my project-wide render convenience function, which sets up various things in the context that are used on every page. (You could also add something to
TEMPLATE_CONTEXT_PROCESSORS to manage this, but that’s slightly less powerful since you’re still accepting a normal template render in that case.)
I want a Pony!
Okay, so I had two problems with this. Firstly, I got thoroughly fed up of copying my
handler500 functions around and remembering to hook them up. Secondly, for the /dev/fort project we needed to emit
403 Forbidden regularly, and wanted to style it nicely. 403 is clearly an exceptional response, so it should be generated by raising an exception of some sort in the relevant view function. The alternative is some frankly revolting code:
def view(request, slug): obj = get_object_by_slug(slug) if not request_allowed_to_see_object(request, obj): return HttpResponseForbidden(...)
Urgh. What I want to do is this:
def view_request, slug): obj = get_object_or_403(MyModel, slug=slug)
which will then raise a (new)
Http403 exception automatically if there isn’t a matching object. Then I want the exception to trigger a nice rendered template configured elsewhere in the project.
This little unicorn went “501, 501, 501” all the way home
If you don’t care about 403, then maybe you just want to use HTTP status codes as easter eggs, in the way that Wildlife Near You does; some of its species are noted as 501 Not Implemented (in this universe).
get_object_or_403 (a disgression)
If you’ve used Django a bit you’ll have encountered the convenience function
get_object_or_404, which looks up a model instance based on parameters to the function, and raises
Http404 for you if there’s no matching instance.
get_object_or_403 does exactly the same, but raises a different exception. Combine this with some middleware (which I’ll describe in a minute) and everything works as I want. The only question is: why would I want to raise a 403 when an object isn’t found? Surely that’s what 404 is for?
The answer is: privacy, and more generally: preventing unwanted information disclosure. Say you have a site which allows users (with URLs such as
/user/james) to show support for certain causes. For each cause, there’s a given slug, so “vegetarianism” has the slug
vegetarianism and so on; the page about that user’s support of vegetarianism would then be
/user/james/vegetarianism. Someone’s support of a cause may be public (anyone can see it) or private (only people they specifically approve can see it). This leads to three possible options for each cause, and the “usual” way of representing these using HTTP status codes would be as follows:
- the user supports it publicly, and
- the user supports it privately, and
/user/james/vegetarianismreturns 403 (unless you’re on the list, in which case you get 200 as above)
- the user doesn’t support the cause, and
This is fine when the main point is to hide the details of the user’s support. However if the cause is “international fascism” (
international-fascism), a user supporting privately may not want the fact that they support the cause to be leaked to the general public at all. At this point, either the site should always return 404 to mean “does not support or you aren’t allowed to know”, or the site should always return 403 to mean the same thing.
The HTTP specification actually suggests (weakly) using 404 where you don’t want to admit the reason for denying access, but either way is apparently fine by the book, and in some cases 403 is going to make more sense than 404. However since Django doesn’t give any magic support for raising an exception that means 403, we needed an exception and some middleware to catch it and turn it into a suitable response.
The idea then is that you raise exceptions that are subclasses of
django_custom_rare_http_responses.responses.RareHttpResponse (some of the more useful ones are in
django_custom_rare_http_responses.responses), and the middleware will then render this using a template
http_responses/404.html or whatever. If that template doesn’t exist, it’ll use
http_responses/default.html, and if that doesn’t exist, the app has its own default (make sure that you have
django.template.loaders.app_directories.load_template_source in your
TEMPLATE_LOADERS setting if you want to rely on this).
In order to override the way that the template is rendered, you want to call
django_custom_rare_http_responses.middleware.set_renderer with a callable that takes a request object, a template name and a context dictionary, the last of which is set up with
http_message (in English, taken from
http_response_exception (allowing you to subclass
RareHttpResponse yourself to pass through more information if you need). The default just calls the
render_to_response shortcut in the expected way.
What this means in practice is that you can add
INSTALLED_APPS, than raise
HttpForbidden (for 403),
HttpNotImplemented (for 501) and so on. All you need to do to prettify them is to create a template
http_responses/default.html and away you go.
My Pony only has three legs
This isn’t finished. Firstly, those 500 responses that Django generates because of other exceptions raised by your code aren’t being handled by this app. Secondly, and perhaps more importantly, adding this app currently changes the behaviour of Django in an unexpected fashion. Without it, and when
True, 404s are reported as a helpful backtrace; with it, these are suddenly rendered using your pretty template, which is a little unexpected.
However these are both fairly easy to fix; if it bugs you and I haven’t got round to it, send me a pull request on github once you’re done.