Fixed #22461 -- Added if-unmodified-since support to the condition decorator.
This commit is contained in:
parent
fae551d765
commit
b27db97b23
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
^^^^^
|
^^^^^
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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/')
|
||||||
|
|
Loading…
Reference in New Issue