conditional-view-processing.txt 8.87 KB
Newer Older
1 2 3 4 5 6
===========================
Conditional View Processing
===========================

HTTP clients can send a number of headers to tell the server about copies of a
resource that they have already seen. This is commonly used when retrieving a
7
Web page (using an HTTP ``GET`` request) to avoid sending all the data for
8 9 10 11 12 13 14 15 16 17
something the client has already retrieved. However, the same headers can be
used for all HTTP methods (``POST``, ``PUT``, ``DELETE``, etc).

For each page (response) that Django sends back from a view, it might provide
two HTTP headers: the ``ETag`` header and the ``Last-Modified`` header. These
headers are optional on HTTP responses. They can be set by your view function,
or you can rely on the :class:`~django.middleware.common.CommonMiddleware`
middleware to set the ``ETag`` header.

When the client next requests the same resource, it might send along a header
18 19 20
such as either `If-modified-since`_ or `If-unmodified-since`_, containing the
date of the last modification time it was sent, or either `If-match`_ or
`If-none-match`_, containing the last ``ETag`` it was sent.
21 22 23
If the current version of the page matches the ``ETag`` sent by the client, or
if the resource has not been modified, a 304 status code can be sent back,
instead of a full response, telling the client that nothing has changed.
24 25 26
Depending on the header, if the page has been modified or does not match the
``ETag`` sent by the client, a 412 status code (Precondition Failed) may be
returned.
27

28
.. _If-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
29 30
.. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
.. _If-modified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
31
.. _If-unmodified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28
32

33 34
When you need more fine-grained control you may use per-view conditional
processing functions.
35

36 37 38 39 40
.. versionchanged:: 1.8

    Support for the ``If-unmodified-since`` header was added to conditional
    view processing.

41
.. _conditional-decorators:
42

43 44
The ``condition`` decorator
===========================
45

46 47 48 49 50 51
Sometimes (in fact, quite often) you can create functions to rapidly compute the ETag_
value or the last-modified time for a resource, **without** needing to do all
the computations needed to construct the full view. Django can then use these
functions to provide an "early bailout" option for the view processing.
Telling the client that the content has not been modified since the last
request, perhaps.
52 53 54

.. _ETag: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11

55 56 57 58 59 60 61 62 63 64 65 66 67 68
These two functions are passed as parameters the
``django.views.decorators.http.condition`` decorator. This decorator uses
the two functions (you only need to supply one, if you can't compute both
quantities easily and quickly) to work out if the headers in the HTTP request
match those on the resource. If they don't match, a new copy of the resource
must be computed and your normal view is called.

The ``condition`` decorator's signature looks like this::

    condition(etag_func=None, last_modified_func=None)

The two functions, to compute the ETag and the last modified time, will be
passed the incoming ``request`` object and the same parameters, in the same
order, as the view function they are helping to wrap. The function passed
69 70 71 72
``last_modified_func`` should return a standard datetime value specifying the
last time the resource was modified, or ``None`` if the resource doesn't
exist. The function passed to the ``etag`` decorator should return a string
representing the `Etag`_ for the resource, or ``None`` if it doesn't exist.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

Using this feature usefully is probably best explained with an example.
Suppose you have this pair of models, representing a simple blog system::

    import datetime
    from django.db import models

    class Blog(models.Model):
        ...

    class Entry(models.Model):
        blog = models.ForeignKey(Blog)
        published = models.DateTimeField(default=datetime.datetime.now)
        ...

If the front page, displaying the latest blog entries, only changes when you
add a new blog entry, you can compute the last modified time very quickly. You
need the latest ``published`` date for every entry associated with that blog.
One way to do this would be::

    def latest_entry(request, blog_id):
94
        return Entry.objects.filter(blog=blog_id).latest("published").published
95 96 97

You can then use this function to provide early detection of an unchanged page
for your front page view::
98

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
    from django.views.decorators.http import condition

    @condition(last_modified_func=latest_entry)
    def front_page(request, blog_id):
        ...

Shortcuts for only computing one value
======================================

As a general rule, if you can provide functions to compute *both* the ETag and
the last modified time, you should do so. You don't know which headers any
given HTTP client will send you, so be prepared to handle both. However,
sometimes only one value is easy to compute and Django provides decorators
that handle only ETag or only last-modified computations.

The ``django.views.decorators.http.etag`` and
``django.views.decorators.http.last_modified`` decorators are passed the same
type of functions as the ``condition`` decorator. Their signatures are::

    etag(etag_func)
    last_modified(last_modified_func)

We could write the earlier example, which only uses a last-modified function,
using one of these decorators::

    @last_modified(latest_entry)
    def front_page(request, blog_id):
126 127
        ...

128
...or::
129

130
    def front_page(request, blog_id):
131
        ...
132
    front_page = last_modified(latest_entry)(front_page)
133

134 135
Use ``condition`` when testing both conditions
------------------------------------------------
136

137 138 139
It might look nicer to some people to try and chain the ``etag`` and
``last_modified`` decorators if you want to test both preconditions. However,
this would lead to incorrect behavior.
140

141
::
142 143 144 145 146 147 148 149 150 151 152

    # Bad code. Don't do this!
    @etag(etag_func)
    @last_modified(last_modified_func)
    def my_view(request):
        # ...

    # End of bad code.

The first decorator doesn't know anything about the second and might
answer that the response is not modified even if the second decorators would
153 154
determine otherwise. The ``condition`` decorator uses both callback functions
simultaneously to work out the right action to take.
155 156 157 158

Using the decorators with other HTTP methods
============================================

159
The ``condition`` decorator is useful for more than only ``GET`` and
160
``HEAD`` requests (``HEAD`` requests are the same as ``GET`` in this
161
situation). It can also be used to provide checking for ``POST``,
162 163 164 165 166 167
``PUT`` and ``DELETE`` requests. In these situations, the idea isn't to return
a "not modified" response, but to tell the client that the resource they are
trying to change has been altered in the meantime.

For example, consider the following exchange between the client and server:

168 169 170 171 172 173 174 175 176 177 178 179
1. Client requests ``/foo/``.
2. Server responds with some content with an ETag of ``"abcd1234"``.
3. Client sends an HTTP ``PUT`` request to ``/foo/`` to update the
   resource. It also sends an ``If-Match: "abcd1234"`` header to specify
   the version it is trying to update.
4. Server checks to see if the resource has changed, by computing the ETag
   the same way it does for a ``GET`` request (using the same function).
   If the resource *has* changed, it will return a 412 status code code,
   meaning "precondition failed".
5. Client sends a ``GET`` request to ``/foo/``, after receiving a 412
   response, to retrieve an updated version of the content before updating
   it.
180 181 182

The important thing this example shows is that the same functions can be used
to compute the ETag and last modification values in all situations. In fact,
183
you **should** use the same functions, so that the same values are returned
184 185
every time.

186 187 188 189 190 191 192 193 194 195
Comparison with middleware conditional processing
=================================================

You may notice that Django already provides simple and straightforward
conditional ``GET`` handling via the
:class:`django.middleware.http.ConditionalGetMiddleware` and
:class:`~django.middleware.common.CommonMiddleware`. Whilst certainly being
easy to use and suitable for many situations, those pieces of middleware
functionality have limitations for advanced usage:

196 197 198 199
* They are applied globally to all views in your project
* They don't save you from generating the response itself, which may be
  expensive
* They are only appropriate for HTTP ``GET`` requests.
200 201 202 203 204 205 206 207

You should choose the most appropriate tool for your particular problem here.
If you have a way to compute ETags and modification times quickly and if some
view takes a while to generate the content, you should consider using the
``condition`` decorator described in this document. If everything already runs
fairly quickly, stick to using the middleware and the amount of network
traffic sent back to the clients will still be reduced if the view hasn't
changed.