If both are used simultaneously, everything behaves fine for a regular
django view, which returns `HttpResponse`.
In the case of working with `TemplateResponse`, the decorators are invoked
in the correct order but **applied** out of order.
The difference between the responses is that the `TemplateResponse` has a
callable `render` method and the decorator (based on CacheMiddleware) has
a `process_response` method.
Because of that, we register a new `post_render_callback` here:
https://github.com/django/django/blob/fdf209eab8949ddc345aa0212b349c79fc6fdebb/django/utils/decorators.py#L145
{{{
# Defer running of process_response until after the template
# has been rendered:
if hasattr(middleware, "process_response"):
def callback(response):
return middleware.process_response(request, response)
response.add_post_render_callback(callback)
}}}
In comparison with `never_cache`, which is not based on middleware, we
don't postpone applying the decorator, thus it's always applied before
`cache_page` regardless of their order.
It means, the following definitions are returning the same output, spite
of the different decorators' order:
{{{
# I used here Wagtail's Page model as an example, because their views
return `TemplateResponse`
@method_decorator(never_cache, name="serve")
@method_decorator(cache_page(settings.CACHE_TIME), name="serve")
class SomePage(Page):
...
# or
@method_decorator(cache_page(settings.CACHE_TIME), name="serve")
@method_decorator(never_cache, name="serve")
class SomePage(Page):
...
}}}
This issue was originally posted on Wagtail project, but after figuring
out what the actual issue is, I decided to post it here. For reference and
described debugging process, please go to:
https://github.com/wagtail/wagtail/issues/7666
----
**Workaround**
To temporarily solve the issue, I decided to create a workaround by
redefining what `never_cache` is:
{{{
class NeverCacheMiddleware:
def process_response(self, request, response):
add_never_cache_headers(response)
return response
never_cache = decorator_from_middleware(NeverCacheMiddleware)
}}}
As you can see, it tries to mimic how `cache_page` was implemented.
Because of this it will also `add_post_render_callback` and postpone
applying `add_never_cache_headers`.
----
**Tests**
`views.py`
{{{
from django.http import HttpResponse
from django.template.response import TemplateResponse
from django.views.decorators.cache import cache_page, never_cache
@never_cache
@cache_page(3600)
def never_cache_first_http_response(request):
return HttpResponse("never_cache_first_http_response")
@cache_page(3600)
@never_cache
def cache_page_first_http_response(request):
return HttpResponse("cache_page_first_http_response")
@never_cache
@cache_page(3600)
def never_cache_first_template_response(request):
return TemplateResponse(request, "app/template.html")
@cache_page(3600)
@never_cache
def cache_page_first_template_response(request):
return TemplateResponse(request, "app/template.html")
}}}
`test_views.py`
{{{
from django.test import Client, SimpleTestCase
from freezegun import freeze_time
from email.utils import parsedate_to_datetime
@freeze_time("2022-01-14T10:00:00Z")
class TestHttpResponse(SimpleTestCase):
def test_different_expires_header_value(self):
client = Client()
first_never_cache = client.get("/never-cache-first-http-response")
first_cache_page = client.get("/cache-page-first-http-response")
assert first_never_cache.headers['Expires'] !=
first_cache_page.headers['Expires']
first_never_cache_datetime =
parsedate_to_datetime(first_never_cache.headers['Expires'])
first_cache_page_datetime =
parsedate_to_datetime(first_cache_page.headers['Expires'])
# Check that `cache_page` correcty sets the Expires header (to be
+1 hour)
assert (first_never_cache_datetime -
first_cache_page_datetime).seconds == 3600
@freeze_time("2022-01-14T10:00:00Z")
class TestTemplateResponse(SimpleTestCase):
def test_different_expires_header_value(self):
client = Client()
first_never_cache = client.get("/never-cache-first-template-
response")
first_cache_page = client.get("/cache-page-first-template-
response")
# Fails, because headers have the same value
assert first_never_cache.headers['Expires'] !=
first_cache_page.headers['Expires']
}}}
fails with:
{{{
E AssertionError: assert 'Fri, 14 Jan 2022 10:00:00 GMT' != 'Fri, 14
Jan 2022 10:00:00 GMT'
app/test_views.py:28: AssertionError
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/33588>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
* version: 3.2 => 4.0
--
Ticket URL: <https://code.djangoproject.com/ticket/33588#comment:1>