Browse Source

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

Denis Cornehl 9 years ago
parent
commit
a840710e1e

+ 2 - 0
django/conf/global_settings.py

@@ -23,6 +23,8 @@ DEBUG = False
 DEBUG_PROPAGATE_EXCEPTIONS = False
 
 # 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
 
 # People who get code error notifications.

+ 11 - 2
django/middleware/common.py

@@ -1,4 +1,5 @@
 import re
+import warnings
 
 from django import http
 from django.conf import settings
@@ -8,7 +9,7 @@ from django.urls import is_valid_path
 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, RemovedInDjango21Warning
 from django.utils.encoding import force_text
 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
           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
@@ -115,6 +117,13 @@ class CommonMiddleware(MiddlewareMixin):
                 return self.response_redirect_class(self.get_full_path_with_slash(request))
 
         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'):
                 set_response_etag(response)
 

+ 15 - 2
django/middleware/http.py

@@ -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.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
     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.
     """
@@ -16,6 +19,9 @@ class ConditionalGetMiddleware(MiddlewareMixin):
         if not response.streaming and not response.has_header('Content-Length'):
             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')
         last_modified = response.get('Last-Modified')
         if last_modified:
@@ -30,3 +36,10 @@ class ConditionalGetMiddleware(MiddlewareMixin):
             )
 
         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)

+ 9 - 0
django/utils/cache.py

@@ -22,10 +22,12 @@ import hashlib
 import logging
 import re
 import time
+import warnings
 
 from django.conf import settings
 from django.core.cache import caches
 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.http import (
     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:
         cache_timeout = 0  # Can't have max-age negative
     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):
             response.add_post_render_callback(set_response_etag)
         else:

+ 3 - 0
docs/internals/deprecation.txt

@@ -43,6 +43,9 @@ details on these changes.
 
 * 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:
 
 2.0

+ 12 - 1
docs/ref/middleware.txt

@@ -72,6 +72,12 @@ Adds a few conveniences for perfectionists:
 
     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
 
 Defaults to :class:`~django.http.HttpResponsePermanentRedirect`. Subclass
@@ -166,13 +172,18 @@ Conditional GET middleware
 
 .. 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
 ``If-Modified-Since``, the response is replaced by an
 :class:`~django.http.HttpResponseNotModified`.
 
 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
 -----------------
 

+ 5 - 0
docs/ref/settings.txt

@@ -2532,6 +2532,11 @@ bandwidth but slows down performance. This is used by the
 :class:`~django.middleware.common.CommonMiddleware` and in the :doc:`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
 
 ``USE_I18N``

+ 5 - 0
docs/ref/utils.txt

@@ -65,6 +65,11 @@ need to distinguish caches by the ``Accept-language`` header.
 
         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)
 
     Adds a ``Cache-Control: max-age=0, no-cache, no-store, must-revalidate``

+ 9 - 0
docs/releases/1.11.txt

@@ -327,6 +327,9 @@ Requests and Responses
 * Added the :setting:`SECURE_HSTS_PRELOAD` setting to allow appending the
   ``preload`` directive to the ``Strict-Transport-Security`` header.
 
+* :class:`~django.middleware.http.ConditionalGetMiddleware` now adds the
+  ``ETag`` header to responses.
+
 Serialization
 ~~~~~~~~~~~~~
 
@@ -633,3 +636,9 @@ Miscellaneous
 * :func:`~django.contrib.auth.authenticate` now passes a ``request`` argument
   to the ``authenticate()`` method of authentication backends. Support for
   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.

+ 9 - 12
docs/topics/conditional-view-processing.txt

@@ -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
 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 :class:`~django.middleware.common.CommonMiddleware`
+or you can rely on the :class:`~django.middleware.http.ConditionalGetMiddleware`
 middleware to set the ``ETag`` 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
 =================================================
 
-You may notice that Django already provides simple and straightforward
-conditional ``GET`` handling via the
-:class:`django.middleware.http.ConditionalGetMiddleware` and
-:class:`~django.middleware.common.CommonMiddleware`. While certainly being
-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
-* They don't save you from generating the response itself, which may be
-  expensive
-* They are only appropriate for HTTP ``GET`` requests.
+Django provides simple and straightforward conditional ``GET`` handling via
+:class:`django.middleware.http.ConditionalGetMiddleware`. While being easy to
+use and suitable for many situations, the middleware has limitations for
+advanced usage:
+
+* It's applied globally to all views in your project.
+* It doesn't save you from generating the response, which may be expensive.
+* It's only appropriate for HTTP ``GET`` requests.
 
 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

+ 2 - 2
docs/topics/i18n/translation.txt

@@ -1372,8 +1372,8 @@ URL::
     ]
 
 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
-covered. Otherwise, you can apply :ref:`conditional decorators
+you're using ETags (:class:`~django.middleware.http.ConditionalGetMiddleware`),
+you're already covered. Otherwise, you can apply :ref:`conditional decorators
 <conditional-decorators>`. In the following example, the cache is invalidated
 whenever you restart your application server::
 

+ 2 - 1
docs/topics/performance.txt

@@ -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
-``ETag`` and ``Last-Modified`` headers.
+``ETag`` and ``Last-Modified`` headers. It also calculates and sets an ETag if
+needed.
 
 :class:`~django.middleware.gzip.GZipMiddleware`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 4 - 2
tests/admin_views/tests.py

@@ -35,7 +35,9 @@ from django.urls import NoReverseMatch, resolve, reverse
 from django.utils import formats, six, translation
 from django.utils._os import upath
 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.html import escape
 from django.utils.http import urlencode
@@ -6074,7 +6076,7 @@ class TestETagWithAdminView(SimpleTestCase):
             self.assertEqual(response.status_code, 302)
             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'))
             self.assertEqual(response.status_code, 302)
             self.assertTrue(response.has_header('ETag'))

+ 4 - 2
tests/cache/tests.py

@@ -33,8 +33,8 @@ from django.template import engines
 from django.template.context_processors import csrf
 from django.template.response import TemplateResponse
 from django.test import (
-    RequestFactory, SimpleTestCase, TestCase, TransactionTestCase, mock,
-    override_settings,
+    RequestFactory, SimpleTestCase, TestCase, TransactionTestCase,
+    ignore_warnings, mock, override_settings,
 )
 from django.test.signals import setting_changed
 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"
             )
 
+    @ignore_warnings(category=RemovedInDjango21Warning)  # USE_ETAGS=True
     @override_settings(
         CACHE_MIDDLEWARE_KEY_PREFIX="test",
         CACHE_MIDDLEWARE_SECONDS=60,
@@ -2262,6 +2263,7 @@ class TestWithTemplateResponse(SimpleTestCase):
         response = response.render()
         self.assertFalse(response.has_header('ETag'))
 
+    @ignore_warnings(category=RemovedInDjango21Warning)
     @override_settings(USE_ETAGS=True)
     def test_with_etag(self):
         template = engines['django'].from_string("This is a test")

+ 35 - 1
tests/middleware/tests.py

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