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 Web page (using an HTTP
GET request) to avoid sending all the data for something the client has already retrieved. However, the same headers can be used for all HTTP methods (
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
ConditionalGetMiddleware middleware to set the
When the client next requests the same resource, it might send along a header 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. 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. 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.
When you need more fine-grained control you may use per-view conditional processing functions.
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.
These two functions are passed as parameters to 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.
condition decorator’s signature looks like this:
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
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.
In older versions, the return value from
etag_func() was interpreted as the unquoted part of the ETag. That prevented the use of weak ETags, which have the format
W/"<string>". The return value is now expected to be an ETag as defined by the specification (including the quotes), although the 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:
import datetime from django.db import models class Blog(models.Model): ... class Entry(models.Model): blog = models.ForeignKey(Blog, on_delete=models.CASCADE) 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): return Entry.objects.filter(blog=blog_id).latest("published").published
You can then use this function to provide early detection of an unchanged page for your front page view:
from django.views.decorators.http import condition @condition(last_modified_func=latest_entry) def front_page(request, blog_id): ...
Be careful with the order of decorators
condition() returns a conditional response, any decorators below it will be skipped and won’t apply to the response. Therefore, any decorators that need to apply to both the regular view response and a conditional response must be above
condition(). In particular,
cache_control() should come first because RFC 7232 requires that the headers they set be present on 304 responses.
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.
django.views.decorators.http.last_modified decorators are passed the same type of functions as the
condition decorator. Their signatures are:
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): ...
def front_page(request, blog_id): ... front_page = last_modified(latest_entry)(front_page)
conditionwhen testing both conditions
It might look nicer to some people to try and chain the
last_modified decorators if you want to test both preconditions. However, this would lead to incorrect behavior.
# 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 determine otherwise. The
condition decorator uses both callback functions simultaneously to work out the right action to take.
condition decorator is useful for more than only
HEAD requests (
HEAD requests are the same as
GET in this situation). It can also be used to provide checking for
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:
/foo/to update the resource. It also sends an
If-Match: "abcd1234"header to specify the version it is trying to update.
GETrequest (using the same function). If the resource has changed, it will return a 412 status code, meaning “precondition failed”.
/foo/, after receiving a 412 response, to retrieve an updated version of the content before updating it.
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, you should use the same functions, so that the same values are returned every time.
Django provides simple and straightforward conditional
GET handling via
django.middleware.http.ConditionalGetMiddleware. While being easy to use and suitable for many situations, the middleware has limitations for advanced usage:
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.
© Django Software Foundation and individual contributors
Licensed under the BSD License.