I have been trying to use the Django conditional view processing feature. Basically I want to deny update operations on an entity if it has since been modified by another user, and that seems to work well with the @condition decorator provided by Django.
However there is one issue which I noticed while testing it and later I checked in the Django sources and I found what I think could be a bug, but just wanted to confirm here first before submitting a bug report to Django and a fix.
The decorator is called when a new request comes in, it first calculates the ETag and Last Modified timestamp based on the functions passed in to the decorator, then it passes control over to the get_conditional_response() function. Here the ETag and Last Modified verification would be performed and if they don't match to what's provided in the request, the request will be denied. So far so good.
If the checks pass, the request is allowed and the view is called to process the request and generate the response. While processing the request, if it was an unsafe method e.g. PUT or PATCH, it would update the entity, which would most likely change the ETag and Last Modified values.
However, I noticed that a successful response to PUT or PATCH is sent back with the ETag or Last Modified timestamp calculated before the update was actually performed, and by now these values are invalid or stale. This to me seems wrong. Doing a fresh GET on the same entity then provides the user with updated ETag and Last Modified values in the response.
Don't you think, the condition() decorator should check if the request method is unsafe, then it should do a fresh calculation of ETag and Last Modified after the view processing, and then add the fresh values to the response?
I agree that there's a bug here, though I think it's a bit different from what you describe.
Conditional Requests are defined in RFC 7232, but unfortunately that document is not very explicit about exactly when the conditional headers should be used in a response. It does say:
2.4. When to Use Entity-Tags and Last-Modified Dates
In 200 (OK) responses to GET or HEAD, an origin server...
That might lead one to assume that use of the headers is not defined in other responses.
However, RFC 7231 explicitly allows for the use of ETags in the response to a PUT, matching the new representation (as was your intuition). However, note this caveat:
An origin server MUST NOT send a validator header field (Section 7.2), such as an ETag or Last-Modified field, in a successful response to
PUTunless the request's representation data was saved without any transformation applied to the body...
That is, the client will use the presence or absence of the ETag to determine whether or not its representation (that it just sent as the body to PUT) was the one actually stored. (See this question for more detail on this point.)
However, Django's conditional request API does not allow for making this distinction. Specifically, there's no way for the user to indicate whether or not a view saved the representation without "transformation applied to the body". So there's no way for the condition() decorator to know whether or not adding an ETag is warranted.
So the only thing to do is to be conservative and not return conditional headers at all in this case. Feel free to create a ticket (or else I can do it).
Create a custom middleware to handle etag in the GET/HEAD request. The following code(Django 1.10) shows how to create and process etag using middleware.
Note: Don't enable USE_ETAGS in the settings file
from django.utils.cache import get_conditional_response, set_response_etag
from django.utils.http import unquote_etag
class ETag(object):
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# before view
response = self.get_response(request)
# after view
try:
if request.method in ('GET', 'HEAD'):
if not response.has_header('ETag'):
set_response_etag(response)
etag = response.get('ETag')
return get_conditional_response(
request,
etag=unquote_etag(etag),
last_modified=None,
response=response,
)
except Exception, e:
pass
return response
i am using Django 1.10. If you are using lower versions then override process_response(self, request, response) method with the logic implemented inside the __call__ method. And don't forget to add this to the MIDDLEWARE/MIDDLEWARE_CLASSES in the settings file
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
# myapp contains middleware.py file and
# ETag class is implemented inside the middleware.py file
'myapp.middleware.Etag',
]