diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 6b320f1db5a..9705270b592 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -63,6 +63,7 @@ class UpdateCacheMiddleware(MiddlewareMixin): """ def __init__(self, get_response=None): self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + self.page_timeout = None self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS self.cache = caches[self.cache_alias] @@ -89,15 +90,18 @@ class UpdateCacheMiddleware(MiddlewareMixin): if 'private' in response.get('Cache-Control', ()): return response - # Try to get the timeout from the "max-age" section of the "Cache- - # Control" header before reverting to using the default cache_timeout - # length. - timeout = get_max_age(response) + # Page timeout takes precedence over the "max-age" and the default + # cache timeout. + timeout = self.page_timeout if timeout is None: - timeout = self.cache_timeout - elif timeout == 0: - # max-age was set to 0, don't bother caching. - return response + # The timeout from the "max-age" section of the "Cache-Control" + # header takes precedence over the default cache timeout. + timeout = get_max_age(response) + if timeout is None: + timeout = self.cache_timeout + elif timeout == 0: + # max-age was set to 0, don't cache. + return response patch_response_headers(response, timeout) if timeout and response.status_code == 200: cache_key = learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache) @@ -160,7 +164,7 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): Also used as the hook point for the cache decorator, which is generated using the decorator-from-middleware utility. """ - def __init__(self, get_response=None, cache_timeout=None, **kwargs): + def __init__(self, get_response=None, cache_timeout=None, page_timeout=None, **kwargs): self.get_response = get_response # We need to differentiate between "provided, but using default value", # and "not provided". If the value is provided using a default, then @@ -186,4 +190,5 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): if cache_timeout is None: cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS self.cache_timeout = cache_timeout + self.page_timeout = page_timeout self.cache = caches[self.cache_alias] diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index 9658bd6ba25..773cf0c2c67 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -20,7 +20,7 @@ def cache_page(timeout, *, cache=None, key_prefix=None): into account on caching -- just like the middleware does. """ return decorator_from_middleware_with_args(CacheMiddleware)( - cache_timeout=timeout, cache_alias=cache, key_prefix=key_prefix + page_timeout=timeout, cache_alias=cache, key_prefix=key_prefix, ) diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 9e8dff3456e..064aadd34ac 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -430,6 +430,10 @@ Miscellaneous used with :setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` needs to inherit from :class:`django.views.debug.SafeExceptionReporterFilter`. +* The cache timeout set by :func:`~django.views.decorators.cache.cache_page` + decorator now takes precedence over the ``max-age`` directive from the + ``Cache-Control`` header. + .. _deprecated-features-3.1: Features deprecated in 3.1 diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 29ea8fb001f..bc6b96ecfe1 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -570,6 +570,9 @@ minutes. (Note that we've written it as ``60 * 15`` for the purpose of readability. ``60 * 15`` will be evaluated to ``900`` -- that is, 15 minutes multiplied by 60 seconds per minute.) +The cache timeout set by ``cache_page`` takes precedence over the ``max-age`` +directive from the ``Cache-Control`` header. + The per-view cache, like the per-site cache, is keyed off of the URL. If multiple URLs point at the same view, each URL will be cached separately. Continuing the ``my_view`` example, if your URLconf looks like this:: @@ -605,6 +608,11 @@ The ``key_prefix`` and ``cache`` arguments may be specified together. The ``key_prefix`` argument and the :setting:`KEY_PREFIX ` specified under :setting:`CACHES` will be concatenated. +.. versionchanged:: 3.1 + + In older versions, the ``max-age`` directive from the ``Cache-Control`` + header had precedence over the cache timeout set by ``cache_page``. + Specifying per-view cache in the URLconf ---------------------------------------- diff --git a/tests/cache/tests.py b/tests/cache/tests.py index e99ab408a15..141d782203b 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -2188,6 +2188,29 @@ class CacheMiddlewareTest(SimpleTestCase): response = other_with_prefix_view(request, '16') self.assertEqual(response.content, b'Hello World 16') + def test_cache_page_timeout(self): + # Page timeout takes precedence over the "max-age" section of the + # "Cache-Control". + tests = [ + (1, 3), # max_age < page_timeout. + (3, 1), # max_age > page_timeout. + ] + for max_age, page_timeout in tests: + with self.subTest(max_age=max_age, page_timeout=page_timeout): + view = cache_page(timeout=page_timeout)( + cache_control(max_age=max_age)(hello_world_view) + ) + request = self.factory.get('/view/') + response = view(request, '1') + self.assertEqual(response.content, b'Hello World 1') + time.sleep(1) + response = view(request, '2') + self.assertEqual( + response.content, + b'Hello World 1' if page_timeout > max_age else b'Hello World 2', + ) + cache.clear() + def test_cached_control_private_not_cached(self): """Responses with 'Cache-Control: private' are not cached.""" view_with_private_cache = cache_page(3)(cache_control(private=True)(hello_world_view))