Skip to content

Another take on content negotiation

Published on: November 29, 2008    Categories: Django, Python

Today my co-worker Daniel posted a class which performs content negotiation for Django views, allowing you to write a single view which returns any of several types of responses (e.g., HTML, JSON, XML) according to the incoming HTTP Accept header. While it’s certainly cool, he notes a couple of issues with it (including the redundancy it introduces in URL configuration).

So let’s see if we can’t come up with a way to work around this.

Holiday decorations

It seems that the best way to approach this is to avoid the need to do too much rewriting in the view and URL code; it’d be nice to simply say, after the fact, what types of responses the view should be able to return without changing the view too much or tweaking the URL configuration over and over. And the natural way to do this in Python is with a decorator, so the goal here should be to write a class which generates decorators which handle the content negotiation. And while we’re at it, let’s cut down on some of the repetition by allowing the class to take the set of content types and template names only once, and then reuse them. Ideally, we’ll end up with something which would work like this:

xml_or_html = MultiResponse({ 'text/html': 'hello.html',
                              'application/xml': 'hello.xml' })

@xml_or_html
def hello(request):
    return { 'message': 'Hello, world!' }

The hello view would then, when called, use the hello.html template and return a Content-Type of text/html if the Accept header states a preference for it, or use the hello.xml and return a Content-Type of application/xml if the client’s Accept header prefers that. The only real difference in the way the view is written is that it doesn’t directly return an HttpResponse; instead it returns a dictionary, and our decorator will deal with the problem of turning that dictionary into a template context, rendering the template and returning the response. So let’s get started on the code.

A decorator generator

We’re going to end up defining a class named MultiResponse, whose instances will be able to act as decorators. The first part of this is easy; we’ll just define the class and give it an __init__ method which takes a dictionary mapping content types to template names:

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

Now we need to make sure instances of MultiResponse can be used as decorators. A Python decorator is normally described as a function which takes another function as an argument, and returns yet another function (whose behavior is slightly different), but in reality the “functions” involved can be any callable Python objects, and it’s easy to make practically anything in Python callable. Generally speaking, in Python some_function(some_argument) is syntactic sugar for some_function.__call__(some_argument), so defining a method named __call__ on our class will make it callable. Let’s start with just that:

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

    def __call__(self):
        pass

Now, what we actually want to do is pass some other callable — a view function — to __call__() in order to decorate that view, so the argument signature actually should be:

def __call__(self, view_func):
    pass

And then to actually decorate the view, we need to define a new function which calls the view (which in this case will return a dictionary instead of an HttpResponse), then chooses a template, renders a response of the appropriate Content-Type using that template and finally returns the response. We can start on that by defining the new function, setting it up with an appropriate argument signature (it needs to accept an HttpRequest and then any number or positional or keyword arguments, since we don’t know what additional arguments the view is expecting), and call the view with those arguments to get the dictionary which will become the context:

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)

(remember that in Python, the * and ** syntax allow a function to receive, and then pass on, any combination of positional and/or keyword arguments)

Now we need to figure out which template and Content-Type to use, based on the request’s Accept header. That header will be in request.META[‘HTTP_ACCEPT’] and will consist of one or more content types (and optional “quality” parameters to state a preference for one type or another) separated by commas. Daniel’s original MultiResponse did some simple parsing to figure this out, but whenever I have to do content negotiation in Python I invariably use Joe Gregorio’s mimeparse.py, which implements the HTTP specification’s rules for determining which content type is “preferred” out of an Accept header. The function we want from mimeparse is best_match(), which takes a list of supported content types and an Accept header, and returns the best supported content type out of the available choices, so we can get the content type from that:

import mimeparse

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)
            content_type = mimeparse.best_match(self.template_mapping.keys(),
                                                request.META['HTTP_ACCEPT'])

This simply uses the keys in the template_mapping dictionary (which, remember, will map content types to template names) as the supported content types, and gets the best match out of them. From there, we can use render_to_response to get an HttpResponse object by looking up the template name for the content type we’re using and passing in the context dictionary obtained from the view:

import mimeparse

from django.shortcuts import render_to_response

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)
            content_type = mimeparse.best_match(self.template_mapping.keys(),
                                                request.META['HTTP_ACCEPT'])
            response = render_to_response(self.template_mapping[content_type],
                                          context_dictionary)

Finally, we can set the outgoing Content-Type header and return the response:

import mimeparse

from django.conf import settings
from django.shortcuts import render_to_response

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)
            content_type = mimeparse.best_match(self.template_mapping.keys(),
                                                request.META['HTTP_ACCEPT'])
            response = render_to_response(self.template_mapping[content_type],
                                          context_dictionary)
            response['Content-Type'] = "%s; charset=%s" % (content_type, settings.DEFAULT_CHARSET)
            return response

Note that the Content-Type header involves two parts: the actual content type, and the character set being used. While you can omit the charset parameter, it’s generally a bad idea to do so (especially if you’re working with XML — see Mark Pilgrim’s “XML on the Web has Failed” for an excellent writeup of some of the issues). The DEFAULT_CHARSET setting in Django provides the character set used by default for HTTP responses, so we just use that as the charset value.

Now we have the fully-defined function our decorator will need to produce, so __call__() can simply return it and we’ll be done:

import mimeparse

from django.conf import settings
from django.shortcuts import render_to_response

class MultiResponse(object):
    def __init__(self, template_mapping):
        self.template_mapping = template_mapping

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)
            content_type = mimeparse.best_match(self.template_mapping.keys(),
                                                request.META['HTTP_ACCEPT'])
            response = render_to_response(self.template_mapping[content_type],
                                          context_dictionary)
            response['Content-Type'] = "%s; charset=%s" % (content_type, settings.DEFAULT_CHARSET)
            return response
        return wrapper

Improving MultiResponse

At this point we’ve met our original goal; we could write view code like the example above, and it would work as expected:

xml_or_html = MultiResponse({ 'text/html': 'hello.html',
                              'application/xml': 'hello.xml' })

@xml_or_html
def hello(request):
    return { 'message': 'Hello, world!' }

Or for one-off uses we could write it like so:

@MultiResponse({ 'text/html': 'hello.html', 'application/xml': 'hello.xml' })
def hello(request):
    return { 'message': 'Hello, world!' }

The hello view would, depending on the incoming Accept header, change change its template and the Content-Type of its response. But with a couple of changes, our MultiResponse class can become a bit more useful:

  1. It’d be nice to give it the ability to use RequestContext to automatically populate common variables we always want to have.
  2. Currently, the eventual decorated view function will lose its docstring, and Django’s built-in admin documentation system won’t be able to show any useful information about it, because we’ve thrown away all the original information about the view we’re decorating.

The first improvement is easy enough; we can simply use the context_instance argument of render_to_response, and have MultiResponse take an argument telling it whether to use RequestContext:

import mimeparse

from django.conf import settings
from django.shortcuts import render_to_response
from django.template import Context
from django.template import RequestContext


class MultiResponse(object):
    def __init__(self, template_mapping, request_context=True):
        self.template_mapping = template_mapping
        self.request_context = request_context

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)
            context_instance = self.request_context and RequestContext(request) or Context()
            content_type = mimeparse.best_match(self.template_mapping.keys(),
                                                request.META['HTTP_ACCEPT'])
            response = render_to_response(self.template_mapping[content_type],
                                          context_dictionary,
                                          context_instance=context_instance)
            response['Content-Type'] = "%s; charset=%s" % (content_type, settings.DEFAULT_CHARSET)
            return response
        return wrapper

The request_context argument to __init__() defaults to True, but allows this to be overridden when needed, and __call__() simply looks at that to determine what type of context to use.

Preserving information about the original view function, so that things like automatic documentation will continue to work, is a bit trickier because it involves fiddling with attributes of the decorated function that don’t normally get fiddled with. Fortunately, Python 2.5 introduced the functools module into the standard library, which contains (among other things) a function called update_wrapper; given a function being returned in a decorator, and the original function being decorated, update_wrapper copies over things like the docstring to the new function. And since this is generally useful (and needed for things like Django’s admin documentation), Django bundles a version of this function in django.utils.functional, backported to work with Python 2.4 and Python 2.3. So we can try to import update_wrapper from the standard library and fall back to Django’s copy if it’s unavailable, then use it to copy over the original view function’s attributes:

try:
    from functools import update_wrapper
except ImportError:
    from django.utils.functional import update_wrapper

import mimeparse

from django.conf import settings
from django.shortcuts import render_to_response
from django.template import Context
from django.template import RequestContext


class MultiResponse(object):
    def __init__(self, template_mapping, request_context=True):
        self.template_mapping = template_mapping
        self.request_context = request_context

    def __call__(self, view_func):
        def wrapper(request, *args, **kwargs):
            context_dictionary = view_func(request, *args, **kwargs)
            context_instance = self.request_context and RequestContext(request) or Context()
            content_type = mimeparse.best_match(self.template_mapping.keys(),
                                                request.META['HTTP_ACCEPT'])
            response = render_to_response(self.template_mapping[content_type],
                                          context_dictionary,
                                          context_instance=context_instance)
            response['Content-Type'] = "%s; charset=%s" % (content_type, settings.DEFAULT_CHARSET)
            return response
        update_wrapper(wrapper, view_func)
        return wrapper

And we’re done.

This sort of system is extremely useful for writing, for example, web-based APIs which often need to tailor their response types to a client’s needs, and for doing so based on nothing more than standard information you can already get from HTTP (so that you don’t need to muddy your URL configuration or require GET arguments to determine the response type), as well as a few other tasks. Since this is an area where different people will probably need very different behaviors, however, I don’t think it necessarily needs to become part of Django; given writeups like Daniel’s and this one as starting points, it ought to be easy to produce solutions tailored to specific needs.