Fixed #26447 -- Deprecated settings.USE_ETAGS in favor of ConditionalGetMiddleware.

This commit is contained in:
Denis Cornehl 2016-04-03 12:15:10 +02:00 committed by Tim Graham
parent 46a3d7604e
commit a840710e1e
15 changed files with 126 additions and 24 deletions

View File

@ -23,6 +23,8 @@ DEBUG = False
DEBUG_PROPAGATE_EXCEPTIONS = False DEBUG_PROPAGATE_EXCEPTIONS = False
# Whether to use the "ETag" header. This saves bandwidth but slows down performance. # Whether to use the "ETag" header. This saves bandwidth but slows down performance.
# Deprecated (RemovedInDjango21Warning) in favor of ConditionalGetMiddleware
# which sets the ETag regardless of this setting.
USE_ETAGS = False USE_ETAGS = False
# People who get code error notifications. # People who get code error notifications.

View File

@ -1,4 +1,5 @@
import re import re
import warnings
from django import http from django import http
from django.conf import settings from django.conf import settings
@ -8,7 +9,7 @@ from django.urls import is_valid_path
from django.utils.cache import ( from django.utils.cache import (
cc_delim_re, get_conditional_response, set_response_etag, cc_delim_re, get_conditional_response, set_response_etag,
) )
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin, RemovedInDjango21Warning
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.six.moves.urllib.parse import urlparse from django.utils.six.moves.urllib.parse import urlparse
@ -34,7 +35,8 @@ class CommonMiddleware(MiddlewareMixin):
- ETags: If the USE_ETAGS setting is set, ETags will be calculated from - ETags: If the USE_ETAGS setting is set, ETags will be calculated from
the entire page content and Not Modified responses will be returned the entire page content and Not Modified responses will be returned
appropriately. appropriately. USE_ETAGS is deprecated in favor of
ConditionalGetMiddleware.
""" """
response_redirect_class = http.HttpResponsePermanentRedirect response_redirect_class = http.HttpResponsePermanentRedirect
@ -115,6 +117,13 @@ class CommonMiddleware(MiddlewareMixin):
return self.response_redirect_class(self.get_full_path_with_slash(request)) return self.response_redirect_class(self.get_full_path_with_slash(request))
if settings.USE_ETAGS and self.needs_etag(response): if settings.USE_ETAGS and self.needs_etag(response):
warnings.warn(
"The USE_ETAGS setting is deprecated in favor of "
"ConditionalGetMiddleware which sets the ETag regardless of "
"the setting. CommonMiddleware won't do ETag processing in "
"Django 2.1.",
RemovedInDjango21Warning
)
if not response.has_header('ETag'): if not response.has_header('ETag'):
set_response_etag(response) set_response_etag(response)

View File

@ -1,4 +1,6 @@
from django.utils.cache import get_conditional_response from django.utils.cache import (
cc_delim_re, get_conditional_response, set_response_etag,
)
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.utils.http import http_date, parse_http_date_safe from django.utils.http import http_date, parse_http_date_safe
@ -7,7 +9,8 @@ class ConditionalGetMiddleware(MiddlewareMixin):
""" """
Handles conditional GET operations. If the response has an ETag or Handles conditional GET operations. If the response has an ETag or
Last-Modified header, and the request has If-None-Match or Last-Modified header, and the request has If-None-Match or
If-Modified-Since, the response is replaced by an HttpNotModified. If-Modified-Since, the response is replaced by an HttpNotModified. An ETag
header is added if needed.
Also sets the Date and Content-Length response-headers. Also sets the Date and Content-Length response-headers.
""" """
@ -16,6 +19,9 @@ class ConditionalGetMiddleware(MiddlewareMixin):
if not response.streaming and not response.has_header('Content-Length'): if not response.streaming and not response.has_header('Content-Length'):
response['Content-Length'] = str(len(response.content)) response['Content-Length'] = str(len(response.content))
if self.needs_etag(response) and not response.has_header('ETag'):
set_response_etag(response)
etag = response.get('ETag') etag = response.get('ETag')
last_modified = response.get('Last-Modified') last_modified = response.get('Last-Modified')
if last_modified: if last_modified:
@ -30,3 +36,10 @@ class ConditionalGetMiddleware(MiddlewareMixin):
) )
return response return response
def needs_etag(self, response):
"""
Return True if an ETag header should be added to response.
"""
cache_control_headers = cc_delim_re.split(response.get('Cache-Control', ''))
return all(header.lower() != 'no-store' for header in cache_control_headers)

View File

@ -22,10 +22,12 @@ import hashlib
import logging import logging
import re import re
import time import time
import warnings
from django.conf import settings from django.conf import settings
from django.core.cache import caches from django.core.cache import caches
from django.http import HttpResponse, HttpResponseNotModified from django.http import HttpResponse, HttpResponseNotModified
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_bytes, force_text, iri_to_uri from django.utils.encoding import force_bytes, force_text, iri_to_uri
from django.utils.http import ( from django.utils.http import (
http_date, parse_etags, parse_http_date_safe, quote_etag, http_date, parse_etags, parse_http_date_safe, quote_etag,
@ -242,6 +244,13 @@ def patch_response_headers(response, cache_timeout=None):
if cache_timeout < 0: if cache_timeout < 0:
cache_timeout = 0 # Can't have max-age negative cache_timeout = 0 # Can't have max-age negative
if settings.USE_ETAGS and not response.has_header('ETag'): if settings.USE_ETAGS and not response.has_header('ETag'):
warnings.warn(
"The USE_ETAGS setting is deprecated in favor of "
"ConditionalGetMiddleware which sets the ETag regardless of the "
"setting. patch_response_headers() won't do ETag processing in "
"Django 2.1.",
RemovedInDjango21Warning
)
if hasattr(response, 'render') and callable(response.render): if hasattr(response, 'render') and callable(response.render):
response.add_post_render_callback(set_response_etag) response.add_post_render_callback(set_response_etag)
else: else:

View File

@ -43,6 +43,9 @@ details on these changes.
* The ``django.db.models.permalink()`` decorator will be removed. * The ``django.db.models.permalink()`` decorator will be removed.
* The ``USE_ETAGS`` setting will be removed. ``CommonMiddleware`` and
``django.utils.cache.patch_response_headers()`` will no longer set ETags.
.. _deprecation-removed-in-2.0: .. _deprecation-removed-in-2.0:
2.0 2.0

View File

@ -72,6 +72,12 @@ Adds a few conveniences for perfectionists:
Older versions didn't set the ``Content-Length`` header. Older versions didn't set the ``Content-Length`` header.
.. deprecated:: 1.11
The :setting:`USE_ETAGS` setting is deprecated in favor of using
:class:`~django.middleware.http.ConditionalGetMiddleware` for ETag
processing.
.. attribute:: CommonMiddleware.response_redirect_class .. attribute:: CommonMiddleware.response_redirect_class
Defaults to :class:`~django.http.HttpResponsePermanentRedirect`. Subclass Defaults to :class:`~django.http.HttpResponsePermanentRedirect`. Subclass
@ -166,13 +172,18 @@ Conditional GET middleware
.. class:: ConditionalGetMiddleware .. class:: ConditionalGetMiddleware
Handles conditional GET operations. If the response has a ``ETag`` or Handles conditional GET operations. If the response doesn't have an ``ETag``
header, the middleware adds one if needed. If the response has a ``ETag`` or
``Last-Modified`` header, and the request has ``If-None-Match`` or ``Last-Modified`` header, and the request has ``If-None-Match`` or
``If-Modified-Since``, the response is replaced by an ``If-Modified-Since``, the response is replaced by an
:class:`~django.http.HttpResponseNotModified`. :class:`~django.http.HttpResponseNotModified`.
Also sets the ``Date`` and ``Content-Length`` response-headers. Also sets the ``Date`` and ``Content-Length`` response-headers.
.. versionchanged:: 1.11
In older versions, the middleware didn't set the ``ETag`` header.
Locale middleware Locale middleware
----------------- -----------------

View File

@ -2532,6 +2532,11 @@ bandwidth but slows down performance. This is used by the
:class:`~django.middleware.common.CommonMiddleware` and in the :doc:`cache :class:`~django.middleware.common.CommonMiddleware` and in the :doc:`cache
framework </topics/cache>`. framework </topics/cache>`.
.. deprecated:: 1.11
This setting is deprecated in favor of using ``ConditionalGetMiddleware``,
which sets an ETag regardless of this setting.
.. setting:: USE_I18N .. setting:: USE_I18N
``USE_I18N`` ``USE_I18N``

View File

@ -65,6 +65,11 @@ need to distinguish caches by the ``Accept-language`` header.
In older versions, the ``Last-Modified`` header was also set. In older versions, the ``Last-Modified`` header was also set.
.. deprecated:: 1.11
Since the ``USE_ETAGS`` setting is deprecated, this function won't set
the ``ETag`` header when the deprecation ends in Django 2.1.
.. function:: add_never_cache_headers(response) .. function:: add_never_cache_headers(response)
Adds a ``Cache-Control: max-age=0, no-cache, no-store, must-revalidate`` Adds a ``Cache-Control: max-age=0, no-cache, no-store, must-revalidate``

View File

@ -327,6 +327,9 @@ Requests and Responses
* Added the :setting:`SECURE_HSTS_PRELOAD` setting to allow appending the * Added the :setting:`SECURE_HSTS_PRELOAD` setting to allow appending the
``preload`` directive to the ``Strict-Transport-Security`` header. ``preload`` directive to the ``Strict-Transport-Security`` header.
* :class:`~django.middleware.http.ConditionalGetMiddleware` now adds the
``ETag`` header to responses.
Serialization Serialization
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -633,3 +636,9 @@ Miscellaneous
* :func:`~django.contrib.auth.authenticate` now passes a ``request`` argument * :func:`~django.contrib.auth.authenticate` now passes a ``request`` argument
to the ``authenticate()`` method of authentication backends. Support for to the ``authenticate()`` method of authentication backends. Support for
methods that don't accept ``request`` will be removed in Django 2.1. methods that don't accept ``request`` will be removed in Django 2.1.
* The ``USE_ETAGS`` setting is deprecated in favor of
:class:`~django.middleware.http.ConditionalGetMiddleware` which now adds the
``ETag`` header to responses regardless of the setting. ``CommonMiddleware``
and ``django.utils.cache.patch_response_headers()`` will no longer set ETags
when the deprecation ends.

View File

@ -11,7 +11,7 @@ used for all HTTP methods (``POST``, ``PUT``, ``DELETE``, etc.).
For each page (response) that Django sends back from a view, it might provide 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 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, headers are optional on HTTP responses. They can be set by your view function,
or you can rely on the :class:`~django.middleware.common.CommonMiddleware` or you can rely on the :class:`~django.middleware.http.ConditionalGetMiddleware`
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
@ -189,17 +189,14 @@ every time.
Comparison with middleware conditional processing Comparison with middleware conditional processing
================================================= =================================================
You may notice that Django already provides simple and straightforward Django provides simple and straightforward conditional ``GET`` handling via
conditional ``GET`` handling via the :class:`django.middleware.http.ConditionalGetMiddleware`. While being easy to
:class:`django.middleware.http.ConditionalGetMiddleware` and use and suitable for many situations, the middleware has limitations for
:class:`~django.middleware.common.CommonMiddleware`. While certainly being advanced usage:
easy to use and suitable for many situations, those pieces of middleware
functionality have limitations for advanced usage:
* They are applied globally to all views in your project * It's applied globally to all views in your project.
* They don't save you from generating the response itself, which may be * It doesn't save you from generating the response, which may be expensive.
expensive * It's only appropriate for HTTP ``GET`` requests.
* They are only appropriate for HTTP ``GET`` requests.
You should choose the most appropriate tool for your particular problem here. 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 If you have a way to compute ETags and modification times quickly and if some

View File

@ -1372,8 +1372,8 @@ URL::
] ]
Client-side caching will save bandwidth and make your site load faster. If Client-side caching will save bandwidth and make your site load faster. If
you're using ETags (:setting:`USE_ETAGS = True <USE_ETAGS>`), you're already you're using ETags (:class:`~django.middleware.http.ConditionalGetMiddleware`),
covered. Otherwise, you can apply :ref:`conditional decorators you're already covered. Otherwise, you can apply :ref:`conditional decorators
<conditional-decorators>`. In the following example, the cache is invalidated <conditional-decorators>`. In the following example, the cache is invalidated
whenever you restart your application server:: whenever you restart your application server::

View File

@ -262,7 +262,8 @@ that can help optimize your site's performance. They include:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adds support for modern browsers to conditionally GET responses based on the Adds support for modern browsers to conditionally GET responses based on the
``ETag`` and ``Last-Modified`` headers. ``ETag`` and ``Last-Modified`` headers. It also calculates and sets an ETag if
needed.
:class:`~django.middleware.gzip.GZipMiddleware` :class:`~django.middleware.gzip.GZipMiddleware`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -35,7 +35,9 @@ from django.urls import NoReverseMatch, resolve, reverse
from django.utils import formats, six, translation from django.utils import formats, six, translation
from django.utils._os import upath from django.utils._os import upath
from django.utils.cache import get_max_age from django.utils.cache import get_max_age
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import (
RemovedInDjango20Warning, RemovedInDjango21Warning,
)
from django.utils.encoding import force_bytes, force_text, iri_to_uri from django.utils.encoding import force_bytes, force_text, iri_to_uri
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import urlencode from django.utils.http import urlencode
@ -6074,7 +6076,7 @@ class TestETagWithAdminView(SimpleTestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertFalse(response.has_header('ETag')) self.assertFalse(response.has_header('ETag'))
with self.settings(USE_ETAGS=True): with self.settings(USE_ETAGS=True), ignore_warnings(category=RemovedInDjango21Warning):
response = self.client.get(reverse('admin:index')) response = self.client.get(reverse('admin:index'))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(response.has_header('ETag')) self.assertTrue(response.has_header('ETag'))

View File

@ -33,8 +33,8 @@ from django.template import engines
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import ( from django.test import (
RequestFactory, SimpleTestCase, TestCase, TransactionTestCase, mock, RequestFactory, SimpleTestCase, TestCase, TransactionTestCase,
override_settings, ignore_warnings, mock, override_settings,
) )
from django.test.signals import setting_changed from django.test.signals import setting_changed
from django.utils import six, timezone, translation from django.utils import six, timezone, translation
@ -1856,6 +1856,7 @@ class CacheI18nTest(TestCase):
"Cache keys should include the time zone name when time zones are active" "Cache keys should include the time zone name when time zones are active"
) )
@ignore_warnings(category=RemovedInDjango21Warning) # USE_ETAGS=True
@override_settings( @override_settings(
CACHE_MIDDLEWARE_KEY_PREFIX="test", CACHE_MIDDLEWARE_KEY_PREFIX="test",
CACHE_MIDDLEWARE_SECONDS=60, CACHE_MIDDLEWARE_SECONDS=60,
@ -2262,6 +2263,7 @@ class TestWithTemplateResponse(SimpleTestCase):
response = response.render() response = response.render()
self.assertFalse(response.has_header('ETag')) self.assertFalse(response.has_header('ETag'))
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_with_etag(self): def test_with_etag(self):
template = engines['django'].from_string("This is a test") template = engines['django'].from_string("This is a test")

View File

@ -20,8 +20,11 @@ from django.middleware.common import (
) )
from django.middleware.gzip import GZipMiddleware from django.middleware.gzip import GZipMiddleware
from django.middleware.http import ConditionalGetMiddleware from django.middleware.http import ConditionalGetMiddleware
from django.test import RequestFactory, SimpleTestCase, override_settings from django.test import (
RequestFactory, SimpleTestCase, ignore_warnings, override_settings,
)
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.six.moves import range from django.utils.six.moves import range
from django.utils.six.moves.urllib.parse import quote from django.utils.six.moves.urllib.parse import quote
@ -256,12 +259,14 @@ class CommonMiddlewareTest(SimpleTestCase):
# ETag + If-Not-Modified support tests # ETag + If-Not-Modified support tests
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_etag(self): def test_etag(self):
req = HttpRequest() req = HttpRequest()
res = HttpResponse('content') res = HttpResponse('content')
self.assertTrue(CommonMiddleware().process_response(req, res).has_header('ETag')) self.assertTrue(CommonMiddleware().process_response(req, res).has_header('ETag'))
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_etag_streaming_response(self): def test_etag_streaming_response(self):
req = HttpRequest() req = HttpRequest()
@ -269,12 +274,14 @@ class CommonMiddlewareTest(SimpleTestCase):
res['ETag'] = 'tomatoes' res['ETag'] = 'tomatoes'
self.assertEqual(CommonMiddleware().process_response(req, res).get('ETag'), 'tomatoes') self.assertEqual(CommonMiddleware().process_response(req, res).get('ETag'), 'tomatoes')
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_no_etag_streaming_response(self): def test_no_etag_streaming_response(self):
req = HttpRequest() req = HttpRequest()
res = StreamingHttpResponse(['content']) res = StreamingHttpResponse(['content'])
self.assertFalse(CommonMiddleware().process_response(req, res).has_header('ETag')) self.assertFalse(CommonMiddleware().process_response(req, res).has_header('ETag'))
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_no_etag_no_store_cache(self): def test_no_etag_no_store_cache(self):
req = HttpRequest() req = HttpRequest()
@ -282,6 +289,7 @@ class CommonMiddlewareTest(SimpleTestCase):
res['Cache-Control'] = 'No-Cache, No-Store, Max-age=0' res['Cache-Control'] = 'No-Cache, No-Store, Max-age=0'
self.assertFalse(CommonMiddleware().process_response(req, res).has_header('ETag')) self.assertFalse(CommonMiddleware().process_response(req, res).has_header('ETag'))
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_etag_extended_cache_control(self): def test_etag_extended_cache_control(self):
req = HttpRequest() req = HttpRequest()
@ -289,6 +297,7 @@ class CommonMiddlewareTest(SimpleTestCase):
res['Cache-Control'] = 'my-directive="my-no-store"' res['Cache-Control'] = 'my-directive="my-no-store"'
self.assertTrue(CommonMiddleware().process_response(req, res).has_header('ETag')) self.assertTrue(CommonMiddleware().process_response(req, res).has_header('ETag'))
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
def test_if_none_match(self): def test_if_none_match(self):
first_req = HttpRequest() first_req = HttpRequest()
@ -502,6 +511,30 @@ class ConditionalGetMiddlewareTest(SimpleTestCase):
# Tests for the ETag header # Tests for the ETag header
def test_middleware_calculates_etag(self):
self.assertNotIn('ETag', self.resp)
self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
self.assertEqual(self.resp.status_code, 200)
self.assertNotEqual('', self.resp['ETag'])
def test_middleware_wont_overwrite_etag(self):
self.resp['ETag'] = 'eggs'
self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
self.assertEqual(self.resp.status_code, 200)
self.assertEqual('eggs', self.resp['ETag'])
def test_no_etag_streaming_response(self):
res = StreamingHttpResponse(['content'])
self.assertFalse(ConditionalGetMiddleware().process_response(self.req, res).has_header('ETag'))
def test_no_etag_no_store_cache(self):
self.resp['Cache-Control'] = 'No-Cache, No-Store, Max-age=0'
self.assertFalse(ConditionalGetMiddleware().process_response(self.req, self.resp).has_header('ETag'))
def test_etag_extended_cache_control(self):
self.resp['Cache-Control'] = 'my-directive="my-no-store"'
self.assertTrue(ConditionalGetMiddleware().process_response(self.req, self.resp).has_header('ETag'))
def test_if_none_match_and_no_etag(self): def test_if_none_match_and_no_etag(self):
self.req.META['HTTP_IF_NONE_MATCH'] = 'spam' self.req.META['HTTP_IF_NONE_MATCH'] = 'spam'
self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp) self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
@ -796,6 +829,7 @@ class GZipMiddlewareTest(SimpleTestCase):
self.assertIsNone(r.get('Content-Encoding')) self.assertIsNone(r.get('Content-Encoding'))
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(USE_ETAGS=True) @override_settings(USE_ETAGS=True)
class ETagGZipMiddlewareTest(SimpleTestCase): class ETagGZipMiddlewareTest(SimpleTestCase):
""" """