Conditional View Processing

HTTP clients can send a number of headers to tell the server about copies of aresource that they have already seen. This is commonly used when retrieving aWeb page (using an HTTP GET request) to avoid sending all the data forsomething the client has already retrieved. However, the same headers can beused for all HTTP methods (POST, PUT, DELETE, etc.).

For each page (response) that Django sends back from a view, it might providetwo HTTP headers: the ETag header and the Last-Modified header. Theseheaders are optional on HTTP responses. They can be set by your view function,or you can rely on the ConditionalGetMiddlewaremiddleware to set the ETag header.

When the client next requests the same resource, it might send along a headersuch as either If-modified-since or If-unmodified-since, containing thedate of the last modification time it was sent, or either If-match orIf-none-match, containing the last ETag it was sent.If the current version of the page matches the ETag sent by the client, orif 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.Depending on the header, if the page has been modified or does not match theETag sent by the client, a 412 status code (Precondition Failed) may bereturned.

When you need more fine-grained control you may use per-view conditionalprocessing functions.

The condition decorator

Sometimes (in fact, quite often) you can create functions to rapidly compute the ETagvalue or the last-modified time for a resource, without needing to do allthe computations needed to construct the full view. Django can then use thesefunctions to provide an "early bailout" option for the view processing.Telling the client that the content has not been modified since the lastrequest, perhaps.

These two functions are passed as parameters to thedjango.views.decorators.http.condition decorator. This decorator usesthe two functions (you only need to supply one, if you can't compute bothquantities easily and quickly) to work out if the headers in the HTTP requestmatch those on the resource. If they don't match, a new copy of the resourcemust be computed and your normal view is called.

The condition decorator's signature looks like this:

  1. condition(etag_func=None, last_modified_func=None)

The two functions, to compute the ETag and the last modified time, will bepassed the incoming request object and the same parameters, in the sameorder, as the view function they are helping to wrap. The function passedlast_modified_func should return a standard datetime value specifying thelast time the resource was modified, or None if the resource doesn'texist. The function passed to the etag decorator should return a stringrepresenting the ETag for the resource, or None if it doesn't exist.

The decorator sets the ETag and Last-Modified headers on the responseif they are not already set by the view and if the request's method is safe(GET or HEAD).

Changed in Django 1.11:In older versions, the return value from etag_func() was interpreted asthe unquoted part of the ETag. That prevented the use of weak ETags, whichhave the format W/"<string>". The return value is now expected to bean ETag as defined by the specification (including the quotes), althoughthe unquoted format is also accepted for backwards compatibility.

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

  1. import datetime
  2. from django.db import models
  3.  
  4. class Blog(models.Model):
  5. ...
  6.  
  7. class Entry(models.Model):
  8. blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
  9. published = models.DateTimeField(default=datetime.datetime.now)
  10. ...

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

  1. def latest_entry(request, blog_id):
  2. return Entry.objects.filter(blog=blog_id).latest("published").published

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

  1. from django.views.decorators.http import condition
  2.  
  3. @condition(last_modified_func=latest_entry)
  4. def front_page(request, blog_id):
  5. ...

Be careful with the order of decorators

When condition() returns a conditional response, any decorators belowit will be skipped and won't apply to the response. Therefore, anydecorators that need to apply to both the regular view response and aconditional response must be above condition(). In particular,vary_on_cookie(),vary_on_headers(), andcache_control() should come firstbecause RFC 7232 requires that the headers theyset be present on 304 responses.

Shortcuts for only computing one value

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

The django.views.decorators.http.etag anddjango.views.decorators.http.last_modified decorators are passed the sametype of functions as the condition decorator. Their signatures are:

  1. etag(etag_func)
  2. last_modified(last_modified_func)

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

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

…or:

  1. def front_page(request, blog_id):
  2. ...
  3. front_page = last_modified(latest_entry)(front_page)

Use condition when testing both conditions

It might look nicer to some people to try and chain the etag andlast_modified decorators if you want to test both preconditions. However,this would lead to incorrect behavior.

  1. # Bad code. Don't do this!
  2. @etag(etag_func)
  3. @last_modified(last_modified_func)
  4. def my_view(request):
  5. # ...
  6.  
  7. # End of bad code.

The first decorator doesn't know anything about the second and mightanswer that the response is not modified even if the second decorators woulddetermine otherwise. The condition decorator uses both callback functionssimultaneously to work out the right action to take.

Using the decorators with other HTTP methods

The condition decorator is useful for more than only GET andHEAD requests (HEAD requests are the same as GET in thissituation). It can also be used to provide checking for POST,PUT and DELETE requests. In these situations, the idea isn't to returna "not modified" response, but to tell the client that the resource they aretrying to change has been altered in the meantime.

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

  • 用户请求/foo/[]($7a3b492ffa9f1e24.md#id1)。
  • Server responds with some content with an ETag of "abcd1234".
  • Client sends an HTTP PUT request to /foo/ to update theresource. It also sends an If-Match: "abcd1234" header to specifythe version it is trying to update.
  • Server checks to see if the resource has changed, by computing the ETagthe same way it does for a GET request (using the same function).If the resource has changed, it will return a 412 status code,meaning "precondition failed".
  • Client sends a GET request to /foo/, after receiving a 412response, to retrieve an updated version of the content before updatingit.The important thing this example shows is that the same functions can be usedto compute the ETag and last modification values in all situations. In fact,you should use the same functions, so that the same values are returnedevery time.

Validator headers with non-safe request methods

The condition decorator only sets validator headers (ETag andLast-Modified) for safe HTTP methods, i.e. GET and HEAD. If youwish to return them in other cases, set them in your view. SeeRFC 7231#section-4.3.4 to learn about the distinction between setting avalidator header in response to requests made with PUT versus POST.

Comparison with middleware conditional processing

Django provides simple and straightforward conditional GET handling viadjango.middleware.http.ConditionalGetMiddleware. While being easy touse and suitable for many situations, the middleware has limitations foradvanced usage:

  • It's applied globally to all views in your project.
  • It doesn't save you from generating the response, which may be expensive.
  • It's only appropriate for HTTP GET requests.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 someview takes a while to generate the content, you should consider using thecondition decorator described in this document. If everything already runsfairly quickly, stick to using the middleware and the amount of networktraffic sent back to the clients will still be reduced if the view hasn'tchanged.