Fixed #22461 -- Added if-unmodified-since support to the condition decorator.

This commit is contained in:
Thomas Tanner 2014-12-20 22:14:46 +01:00 committed by Tim Graham
parent fae551d765
commit b27db97b23
4 changed files with 111 additions and 32 deletions

View File

@ -52,6 +52,16 @@ require_safe = require_http_methods(["GET", "HEAD"])
require_safe.__doc__ = "Decorator to require that a view only accept safe methods: GET and HEAD." require_safe.__doc__ = "Decorator to require that a view only accept safe methods: GET and HEAD."
def _precondition_failed(request):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request
},
)
return HttpResponse(status=412)
def condition(etag_func=None, last_modified_func=None): def condition(etag_func=None, last_modified_func=None):
""" """
Decorator to support conditional retrieval (or change) for a view Decorator to support conditional retrieval (or change) for a view
@ -81,8 +91,12 @@ def condition(etag_func=None, last_modified_func=None):
if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE") if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
if if_modified_since: if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since) if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if_none_match = request.META.get("HTTP_IF_NONE_MATCH") if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
if_match = request.META.get("HTTP_IF_MATCH") if_match = request.META.get("HTTP_IF_MATCH")
etags = []
if if_none_match or if_match: if if_none_match or if_match:
# There can be more than one ETag in the request, so we # There can be more than one ETag in the request, so we
# consider the list of values. # consider the list of values.
@ -97,21 +111,19 @@ def condition(etag_func=None, last_modified_func=None):
if_match = None if_match = None
# Compute values (if any) for the requested resource. # Compute values (if any) for the requested resource.
if etag_func: def get_last_modified():
res_etag = etag_func(request, *args, **kwargs)
else:
res_etag = None
if last_modified_func: if last_modified_func:
dt = last_modified_func(request, *args, **kwargs) dt = last_modified_func(request, *args, **kwargs)
if dt: if dt:
res_last_modified = timegm(dt.utctimetuple()) return timegm(dt.utctimetuple())
else:
res_last_modified = None res_etag = etag_func(request, *args, **kwargs) if etag_func else None
else: res_last_modified = get_last_modified()
res_last_modified = None
response = None response = None
if not ((if_match and (if_modified_since or if_none_match)) or if not ((if_match and if_modified_since) or
(if_none_match and if_unmodified_since) or
(if_modified_since and if_unmodified_since) or
(if_match and if_none_match)): (if_match and if_none_match)):
# We only get here if no undefined combinations of headers are # We only get here if no undefined combinations of headers are
# specified. # specified.
@ -123,26 +135,20 @@ def condition(etag_func=None, last_modified_func=None):
if request.method in ("GET", "HEAD"): if request.method in ("GET", "HEAD"):
response = HttpResponseNotModified() response = HttpResponseNotModified()
else: else:
logger.warning('Precondition Failed: %s', request.path, response = _precondition_failed(request)
extra={ elif (if_match and ((not res_etag and "*" in etags) or
'status_code': 412, (res_etag and res_etag not in etags) or
'request': request (res_last_modified and if_unmodified_since and
} res_last_modified > if_unmodified_since))):
) response = _precondition_failed(request)
response = HttpResponse(status=412)
elif if_match and ((not res_etag and "*" in etags) or
(res_etag and res_etag not in etags)):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request
}
)
response = HttpResponse(status=412)
elif (not if_none_match and request.method in ("GET", "HEAD") and elif (not if_none_match and request.method in ("GET", "HEAD") and
res_last_modified and if_modified_since and res_last_modified and if_modified_since and
res_last_modified <= if_modified_since): res_last_modified <= if_modified_since):
response = HttpResponseNotModified() response = HttpResponseNotModified()
elif (not if_match and
res_last_modified and if_unmodified_since and
res_last_modified > if_unmodified_since):
response = _precondition_failed(request)
if response is None: if response is None:
response = func(request, *args, **kwargs) response = func(request, *args, **kwargs)

View File

@ -528,6 +528,9 @@ Requests and Responses
<django.http.HttpResponse.setdefault>` method allows setting a header unless <django.http.HttpResponse.setdefault>` method allows setting a header unless
it has already been set. it has already been set.
* The :func:`~django.views.decorators.http.condition` decorator for
conditional view processing now supports the ``If-unmodified-since`` header.
Tests Tests
^^^^^ ^^^^^

View File

@ -15,18 +15,29 @@ or you can rely on the :class:`~django.middleware.common.CommonMiddleware`
middleware to set the ``ETag`` header. middleware to set the ``ETag`` header.
When the client next requests the same resource, it might send along a header When the client next requests the same resource, it might send along a header
such as `If-modified-since`_, containing the date of the last modification such as either `If-modified-since`_ or `If-unmodified-since`_, containing the
time it was sent, or `If-none-match`_, containing the ``ETag`` it was sent. 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 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, 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. 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.
.. _If-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
.. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 .. _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 .. _If-modified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
.. _If-unmodified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28
When you need more fine-grained control you may use per-view conditional When you need more fine-grained control you may use per-view conditional
processing functions. processing functions.
.. versionchanged:: 1.8
Support for the ``If-unmodified-since`` header was added to conditional
view processing.
.. _conditional-decorators: .. _conditional-decorators:
The ``condition`` decorator The ``condition`` decorator
@ -194,4 +205,3 @@ view takes a while to generate the content, you should consider using the
fairly quickly, stick to using the middleware and the amount of network 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 traffic sent back to the clients will still be reduced if the view hasn't
changed. changed.

View File

@ -49,6 +49,20 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/') response = self.client.get('/condition/')
self.assertFullResponse(response) self.assertFullResponse(response)
def test_if_unmodified_since(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_NEWER_STR
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_INVALID_STR
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
def test_if_none_match(self): def test_if_none_match(self):
self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/') response = self.client.get('/condition/')
@ -71,6 +85,7 @@ class ConditionalGet(TestCase):
self.assertEqual(response.status_code, 412) self.assertEqual(response.status_code, 412)
def test_both_headers(self): def test_both_headers(self):
# see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/') response = self.client.get('/condition/')
@ -86,6 +101,32 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/') response = self.client.get('/condition/')
self.assertFullResponse(response) self.assertFullResponse(response)
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.get('/condition/')
self.assertFullResponse(response)
def test_both_headers_2(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
def test_single_condition_1(self): def test_single_condition_1(self):
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified/') response = self.client.get('/condition/last_modified/')
@ -124,6 +165,25 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/last_modified2/') response = self.client.get('/condition/last_modified2/')
self.assertFullResponse(response, check_etag=False) self.assertFullResponse(response, check_etag=False)
def test_single_condition_7(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified/')
self.assertEqual(response.status_code, 412)
response = self.client.get('/condition/etag/')
self.assertFullResponse(response, check_last_modified=False)
def test_single_condition_8(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified/')
self.assertFullResponse(response, check_etag=False)
def test_single_condition_9(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified2/')
self.assertEqual(response.status_code, 412)
response = self.client.get('/condition/etag2/')
self.assertFullResponse(response, check_last_modified=False)
def test_single_condition_head(self): def test_single_condition_head(self):
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.head('/condition/') response = self.client.head('/condition/')