Django Exceptional Middleware
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
exceptional_middleware
.gitignore
LICENSE
MANIFEST.in
README.md
setup.py

README.md

Django Exceptional Middleware

Middleware app that makes it easy to generate HTTP status codes as exceptions, with customisable templates, and making it easy to style them all the same way.

This was originally written at a /dev/fort, based on an initial implementation by Richard Boulton, and subsequently developed at other devforts; it was previously available as django_custom_rare_http_responses, and is now on PyPI as django_exceptional_middleware.

With Django 1.3's Class Based Views, this becomes particularly useful. Future versions of Django are likely to get some kind of support which will replace this approach.

Basic use

Add exceptional_middleware.middleware.ExceptionalMiddleware to MIDDLEWARE_CLASSES, and exceptional_middleware to INSTALLED_APPS. Make sure you have django.template.loaders.app_directories.Loader in your TEMPLATE_LOADERS, and you can then raise the exceptions in exceptional_middleware.responses to get a (basic) error page.

To override the default template, put (eg) http_responses/403.html in a TEMPLATE_DIRS directory (assuming django.template.loaders.filesystem.Loader comes before django.template.loaders.app_directories.Loader in your TEMPLATE_LOADERS).

Fallback templates and template context variables

If you don't want to write a different template for each response code, you can make a default.html template, which is how the automatic templating supplied by the app itself works. Templates are invoked with three context variables:

  • http_code: the HTTP response code
  • http_message: an (English) description of the response code; if you use this in your templates, you probably want to use it as a translation string
  • http_response_exception: the exception that caused this, allowing you to pass further information through

Overriding render mechanism

By default, rendering happens using a vanilla invocation of render_to_response. If you want to override this, for instance to add further variables into the render context, you need to call exceptional_middleware.middleware.set_renderer with a callable that takes three parameters request, template_name and context. These will be as expected for this kind of work; the renderer should return an HttpResponse instance, which will then have the correct status_code set by the middleware before returning to the user agent.

Alternatively, you can subclass ExceptionalMiddleware and override the render() method.

Other features

HTTP 403, 404 & 500

The middleware can catch Django's Http404 and turn it into one of its own (HttpNotFound), so you can use the same render code and template layout instead of having to write a handler404. To enable this, set EXCEPTIONAL_INVASION=True in settings.py.

Similarly, runtime errors will be turned into HttpServerError, so you don't have to write handler500, and PermissionDenied becomes HttpForbidden.

Sadly, you'll still have to install a handler404 to catch 404s generated when the URLconf cannot find a matching URL. You may need a handler500 if you get exceptions during that processing (which may happen for memory issues, for instance, so it's wise to install one). You can use exceptional_middleware.handler404 and exceptional_middleware.handler500 (which will instantiate ExceptionalMiddleware and get it to handle the relevant exception).There's also exceptional_middleware.handler403 for Django versions from 1.4 that have PermissionDenied() as an exception.

Note that EXCEPTIONAL_INVASION may be better than using handlers (except in the cases it cannot manage), because your template will get the original exception object. In practice you're either going to need both, or all three handlers.

DEBUG = True

Only 3xx (redirect) processing happens in the middleware if settings.DEBUG = True.

Sentry integration

Our exceptions are /not/ marked for Sentry to ignore, since that would mean that people are raising our exceptions without having loaded our middleware, which you'd want to know about promptly.

If we catch someone else's exception, we always propagate it out to Sentry, if it's installed; we don't do this for our own exceptions except for 5xx (since they aren't really errors), or the Django 1.3 Http404 exception (similarly).

Extra HTTP headers

Each exception has a headers member, which is a dictionary of HTTP header names to values, which will be added into the final HTTP response.

Although there are no provided classes that use this, you can do something like the following:

class HttpRedirect(RareHttpResponse):
    http_code = 302
    
    def __init__(self, url, *args, **kwargs):
        super(HttpRedirect, self).__init__(*args, **kwargs)
        self.headers['Location'] = url

Augment response

Sometimes you may want to do something else to the response at the end of the cycle on a per-exception basis. To do this, override the augment_response() method on your exception before raising it. It takes the response object:

from exceptional_middleware.responses import HttpConflict
class MyConflict(HttpConflict):

    def augment_response(self, response):
        response.delete_cookie('mycookie')

  def augment_response(self, response):
    response.delete_cookie('mycookie')

Testing

If you include exceptional_middleware.urls into your urlconf, you get pre-made targets that will generate all the standard exceptions, which makes it semi-easy to test your custom templates.

urlpatterns += patterns('',
    url(r'^http/', include('exceptional_middleware.urls')),
)

Then you have to run with DEBUG=False because otherwise only 3xx codes will get exceptional handling (ie: you'll see the "technical" responses, with stacktraces and so on, which isn't what you really want).

Note that if you use django.contrib.staticfiles then you also need to use runserver with --insecure or you won't get any of your static assets. (If you use media files, you need to either use django.contrib.staticfiles.views.serve directly, which has an insecure mode, or give up on having media files appear during testing. django.conf.urls.static.static, the helper function for generating static-serving URLs, only works in debug mode.)

Problems

If you intercept "normal" exceptions rather than letting Django's 500 processing happening, the got_request_exception signal is fired with the ExceptionalMiddleware middleware class as sender, rather than the appropriate handler driving the request. This is because there's no way (short of walking the callstack) of figuring out the right one. Unless someone can explain to me why you need to know the handler in your signal processor, it doesn't seem worth fixing.

James Aylett