[Django] #33588: never_cache and cache_page applied out of order for TemplateResponse

36 views
Skip to first unread message

Django

unread,
Mar 20, 2022, 6:23:55 AM3/20/22
to django-...@googlegroups.com
#33588: never_cache and cache_page applied out of order for TemplateResponse
-------------------------------------+-------------------------------------
Reporter: gastlich | Owner: nobody
Type: Bug | Status: new
Component: Core | Version: 3.2
(Cache system) | Keywords:
Severity: Normal | cache,never_cache,cache_page,TemplateResponse,make_middleware_decorator
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
The `never_cache` decorator is a simple decorator, which applies
`add_never_cache_headers` on a response. On the other hand `cache_page`
decorator is based on `CacheMiddleware` by using
`decorator_from_middleware_with_args` adapter. The impact of this issue is
not applying `cache_page` functionality to some of the Django views.

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.

Django

unread,
Mar 20, 2022, 6:31:58 AM3/20/22
to django-...@googlegroups.com
#33588: never_cache and cache_page applied out of order for TemplateResponse
-------------------------------------+-------------------------------------
Reporter: Jacek Wojna | Owner: nobody
Type: Bug | Status: new
Component: Core (Cache system) | Version: 4.0
Severity: Normal | Resolution:
Keywords: | Triage Stage:
cache,never_cache,cache_page,TemplateResponse,make_middleware_decorator| Unreviewed
Has patch: 0 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacek Wojna):

* version: 3.2 => 4.0


--
Ticket URL: <https://code.djangoproject.com/ticket/33588#comment:1>

Reply all
Reply to author
Forward
0 new messages