From 2c434c350a933a5513ed4e499ce26c472de15961 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 10:34:48 +0200 Subject: [PATCH 1/9] send cached response through UpdateCacheMiddleware to apply the wagtail cache header when using cache_page() --- wagtailcache/cache.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 0d92c75..03cbe84 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -412,10 +412,9 @@ def _wrapped_view_func( ) -> HttpResponse: # Try to fetch an already cached page from wagtail-cache. response = FetchFromCacheMiddleware().process_request(request) - if response: - return response - # Since we don't have a response at this point, process the request. - response = view_func(request, *args, **kwargs) + if response is None: + # Since we don't have a response at this point, process the request. + response = view_func(request, *args, **kwargs) # Cache the response. response = UpdateCacheMiddleware().process_response(request, response) return response From be1e0dfd6717823cb8265b64bfe5df891c84e069 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 10:54:12 +0200 Subject: [PATCH 2/9] separate max age setting --- wagtailcache/cache.py | 8 +++++--- wagtailcache/settings.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 03cbe84..c238cf7 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -164,7 +164,7 @@ def _get_cache_key(r: WSGIRequest, c: BaseCache) -> str: def _learn_cache_key( - r: WSGIRequest, s: HttpResponse, t: int, c: BaseCache + r: WSGIRequest, s: HttpResponse, t: Optional[int], c: BaseCache ) -> str: """ Wrapper for Django's learn_cache_key which first strips specific @@ -327,10 +327,12 @@ def process_response( # ``Cache-Control`` header before reverting to using the cache's # default. timeout = get_max_age(response) + max_age = timeout if timeout is None: timeout = self._wagcache.default_timeout - patch_response_headers(response, timeout) - if timeout: + max_age = wagtailcache_settings.WAGTAIL_CACHE_MAX_AGE + patch_response_headers(response, max_age) + if timeout != 0: try: cache_key = _learn_cache_key( request, response, timeout, self._wagcache diff --git a/wagtailcache/settings.py b/wagtailcache/settings.py index d56c9a6..380ddba 100644 --- a/wagtailcache/settings.py +++ b/wagtailcache/settings.py @@ -12,6 +12,7 @@ class _DefaultSettings: WAGTAIL_CACHE_BACKEND = "default" WAGTAIL_CACHE_HEADER = "X-Wagtail-Cache" WAGTAIL_CACHE_IGNORE_COOKIES = True + WAGTAIL_CACHE_MAX_AGE = 5 * 60 WAGTAIL_CACHE_IGNORE_QS = [ r"^_bta_.*$", # Bronto r"^_ga$", # Google Analytics From e8bc29bdfbb349d0a455a5572212aa1e1983c3a7 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 12:30:15 +0200 Subject: [PATCH 3/9] set Cache-Control to no-cache rather than max-age= when WAGTAIL_CACHE_MAX_AGE is set to None --- wagtailcache/cache.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index c238cf7..6bed573 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -17,7 +17,7 @@ from django.core.handlers.wsgi import WSGIRequest from django.http.response import HttpResponse from django.template.response import SimpleTemplateResponse -from django.utils.cache import cc_delim_re +from django.utils.cache import cc_delim_re, patch_cache_control from django.utils.cache import get_cache_key from django.utils.cache import get_max_age from django.utils.cache import has_vary_header @@ -331,7 +331,11 @@ def process_response( if timeout is None: timeout = self._wagcache.default_timeout max_age = wagtailcache_settings.WAGTAIL_CACHE_MAX_AGE - patch_response_headers(response, max_age) + if max_age is None: + patch_cache_control(response, no_cache=True) + else: + patch_response_headers(response, max_age) + if timeout != 0: try: cache_key = _learn_cache_key( From 5ef4a664b97df93b88d27ecbb71c374d62f301e7 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 12:49:08 +0200 Subject: [PATCH 4/9] fix exception on cache settings page if the cache timeout is set to None --- wagtailcache/templates/wagtailcache/index.html | 6 +++++- wagtailcache/templatetags/wagtailcache_tags.py | 13 +------------ wagtailcache/views.py | 3 +++ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/wagtailcache/templates/wagtailcache/index.html b/wagtailcache/templates/wagtailcache/index.html index 58aaca0..63f9d80 100644 --- a/wagtailcache/templates/wagtailcache/index.html +++ b/wagtailcache/templates/wagtailcache/index.html @@ -25,7 +25,11 @@

{% trans "Status" %}

{% trans "ENABLED" %}

- {% trans "Cached pages are automatically refreshed every" %} {% cache_timeout %}.
+ {% if cache_timeout == None %} + {% trans "Cached pages do not expire." %}
+ {% else %} + {% trans "Cached pages are automatically refreshed every" %} {{ cache_timeout|seconds_to_readable }}.
+ {% endif %} {% trans "To modify this, change the TIMEOUT value of the cache backend in the project settings." %}

diff --git a/wagtailcache/templatetags/wagtailcache_tags.py b/wagtailcache/templatetags/wagtailcache_tags.py index c1e75b9..63f6674 100644 --- a/wagtailcache/templatetags/wagtailcache_tags.py +++ b/wagtailcache/templatetags/wagtailcache_tags.py @@ -6,10 +6,10 @@ from wagtailcache.settings import wagtailcache_settings - register = template.Library() +@register.filter def seconds_to_readable(seconds: int) -> str: """ Converts int seconds to a human readable string. @@ -43,14 +43,3 @@ def get_wagtailcache_setting(value: str) -> Optional[object]: Returns a wagtailcache Django setting, or default. """ return getattr(wagtailcache_settings, value, None) - - -@register.simple_tag -def cache_timeout() -> str: - """ - Returns the wagtailcache timeout in human readable format. - """ - timeout = caches[ - wagtailcache_settings.WAGTAIL_CACHE_BACKEND - ].default_timeout - return seconds_to_readable(timeout) diff --git a/wagtailcache/views.py b/wagtailcache/views.py index 30b5a70..d0e1fd9 100644 --- a/wagtailcache/views.py +++ b/wagtailcache/views.py @@ -26,6 +26,9 @@ def index(request): "wagtailcache/index.html", { "keyring": keyring, + "cache_timeout": caches[ + wagtailcache_settings.WAGTAIL_CACHE_BACKEND + ].default_timeout }, ) From a083f71a91decfb5f2b73f1c6074052f71f74661 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 12:52:19 +0200 Subject: [PATCH 5/9] fix formatting etc --- wagtailcache/cache.py | 14 ++------------ wagtailcache/templatetags/wagtailcache_tags.py | 2 +- wagtailcache/views.py | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 6bed573..8e9fe8c 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -17,11 +17,12 @@ from django.core.handlers.wsgi import WSGIRequest from django.http.response import HttpResponse from django.template.response import SimpleTemplateResponse -from django.utils.cache import cc_delim_re, patch_cache_control +from django.utils.cache import cc_delim_re from django.utils.cache import get_cache_key from django.utils.cache import get_max_age from django.utils.cache import has_vary_header from django.utils.cache import learn_cache_key +from django.utils.cache import patch_cache_control from django.utils.cache import patch_response_headers from django.utils.deprecation import MiddlewareMixin from wagtail import hooks @@ -191,7 +192,6 @@ def __init__(self, get_response=None): def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: if not wagtailcache_settings.WAGTAIL_CACHE: return None - # Check if request is cacheable # Only cache GET and HEAD requests. # Don't cache requests that are previews. @@ -203,7 +203,6 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: and not getattr(request, "is_preview", False) and not (hasattr(request, "user") and request.user.is_authenticated) ) - # Allow the user to override our caching decision. for fn in hooks.get_hooks("is_request_cacheable"): result = fn(request, is_cacheable) @@ -214,16 +213,13 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: setattr(request, "_wagtailcache_update", False) setattr(request, "_wagtailcache_skip", True) return None # Don't bother checking the cache. - # Try and get the cached response. try: cache_key = _get_cache_key(request, self._wagcache) - # No cache information available, need to rebuild. if cache_key is None: setattr(request, "_wagtailcache_update", True) return None - # We have a key, get the cached response. response = self._wagcache.get(cache_key) @@ -233,12 +229,10 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: setattr(request, "_wagtailcache_error", True) logger.exception("Could not fetch page from cache backend.") return None - # No cache information available, need to rebuild. if response is None: setattr(request, "_wagtailcache_update", True) return None - # Hit. Return cached response. setattr(request, "_wagtailcache_update", False) return response @@ -288,7 +282,6 @@ def process_response( _chop_response_vary(request, response) # We don't need to update the cache, just return. return response - # Check if the response is cacheable # Don't cache private or no-cache responses. # Do cache 200, 301, 302, 304, and 404 codes so that wagtail doesn't @@ -308,19 +301,16 @@ def process_response( and has_vary_header(response, "Cookie") ) ) - # Allow the user to override our caching decision. for fn in hooks.get_hooks("is_response_cacheable"): result = fn(response, is_cacheable) if isinstance(result, bool): is_cacheable = result - # If we are not allowed to cache the response, just return. if not is_cacheable: # Add response header to indicate this was intentionally not cached. _patch_header(response, Status.SKIP) return response - # Potentially remove the ``Vary: Cookie`` header. _chop_response_vary(request, response) # Try to get the timeout from the ``max-age`` section of the diff --git a/wagtailcache/templatetags/wagtailcache_tags.py b/wagtailcache/templatetags/wagtailcache_tags.py index 63f6674..48404d1 100644 --- a/wagtailcache/templatetags/wagtailcache_tags.py +++ b/wagtailcache/templatetags/wagtailcache_tags.py @@ -1,11 +1,11 @@ from typing import Optional from django import template -from django.core.cache import caches from django.utils.translation import gettext_lazy as _ from wagtailcache.settings import wagtailcache_settings + register = template.Library() diff --git a/wagtailcache/views.py b/wagtailcache/views.py index d0e1fd9..f1b2af6 100644 --- a/wagtailcache/views.py +++ b/wagtailcache/views.py @@ -28,7 +28,7 @@ def index(request): "keyring": keyring, "cache_timeout": caches[ wagtailcache_settings.WAGTAIL_CACHE_BACKEND - ].default_timeout + ].default_timeout, }, ) From 31148f6101ab2d5a9d357fceed50dc8865c0690a Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 12:14:17 +0200 Subject: [PATCH 6/9] set etag on responses and return Not Modified when the If-None-Match header matches the etag of the response in the cache --- wagtailcache/cache.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 8e9fe8c..41014c4 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -17,7 +17,8 @@ from django.core.handlers.wsgi import WSGIRequest from django.http.response import HttpResponse from django.template.response import SimpleTemplateResponse -from django.utils.cache import cc_delim_re +from django.utils.cache import cc_delim_re, set_response_etag, \ + patch_cache_control from django.utils.cache import get_cache_key from django.utils.cache import get_max_age from django.utils.cache import has_vary_header @@ -25,6 +26,7 @@ from django.utils.cache import patch_cache_control from django.utils.cache import patch_response_headers from django.utils.deprecation import MiddlewareMixin +from django.utils.http import parse_etags from wagtail import hooks from wagtailcache.settings import wagtailcache_settings @@ -233,8 +235,17 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: if response is None: setattr(request, "_wagtailcache_update", True) return None - # Hit. Return cached response. + + # Hit. Return cached response or Not Modified. setattr(request, "_wagtailcache_update", False) + response: HttpResponse + if "If-None-Match" in request.headers and "Etag" in response.headers and \ + response.headers["Etag"] in parse_etags( + request.headers["If-None-Match"]): + not_modified = HttpResponse(status=304) + not_modified.headers["Etag"] = response.headers["Etag"] + # TODO: Cache-Control and Expires? + return not_modified return response @@ -325,6 +336,7 @@ def process_response( patch_cache_control(response, no_cache=True) else: patch_response_headers(response, max_age) + patch_cache_control(response, must_revalidate=True) if timeout != 0: try: @@ -344,13 +356,20 @@ def process_response( uri_keys.append(cache_key) keyring[uri] = uri_keys self._wagcache.set("keyring", keyring) + + def set_etag(r): + if "Etag" not in r.headers: + set_response_etag(r) + if isinstance(response, SimpleTemplateResponse): def callback(r): + set_etag(response) self._wagcache.set(cache_key, r, timeout) response.add_post_render_callback(callback) else: + set_etag(response) self._wagcache.set(cache_key, response, timeout) # Add a response header to indicate this was a cache miss. _patch_header(response, Status.MISS) From db965133722d3355e7eead2375b8a3ece7053cef Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 12:32:24 +0200 Subject: [PATCH 7/9] fix formatting --- wagtailcache/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 41014c4..cf98799 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -241,7 +241,7 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: response: HttpResponse if "If-None-Match" in request.headers and "Etag" in response.headers and \ response.headers["Etag"] in parse_etags( - request.headers["If-None-Match"]): + request.headers["If-None-Match"]): not_modified = HttpResponse(status=304) not_modified.headers["Etag"] = response.headers["Etag"] # TODO: Cache-Control and Expires? From 63d1c0b369bba423ca27a9ce4b41b38f07dbe342 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Tue, 16 Jul 2024 12:56:19 +0200 Subject: [PATCH 8/9] fix formatting --- wagtailcache/cache.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index cf98799..44664e3 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -17,14 +17,14 @@ from django.core.handlers.wsgi import WSGIRequest from django.http.response import HttpResponse from django.template.response import SimpleTemplateResponse -from django.utils.cache import cc_delim_re, set_response_etag, \ - patch_cache_control +from django.utils.cache import cc_delim_re from django.utils.cache import get_cache_key from django.utils.cache import get_max_age from django.utils.cache import has_vary_header from django.utils.cache import learn_cache_key from django.utils.cache import patch_cache_control from django.utils.cache import patch_response_headers +from django.utils.cache import set_response_etag from django.utils.deprecation import MiddlewareMixin from django.utils.http import parse_etags from wagtail import hooks @@ -223,7 +223,7 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: setattr(request, "_wagtailcache_update", True) return None # We have a key, get the cached response. - response = self._wagcache.get(cache_key) + response: HttpResponse = self._wagcache.get(cache_key) except Exception: # If the cache backend is currently unresponsive or errors out, @@ -235,13 +235,14 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: if response is None: setattr(request, "_wagtailcache_update", True) return None - # Hit. Return cached response or Not Modified. setattr(request, "_wagtailcache_update", False) - response: HttpResponse - if "If-None-Match" in request.headers and "Etag" in response.headers and \ - response.headers["Etag"] in parse_etags( - request.headers["If-None-Match"]): + if ( + "If-None-Match" in request.headers + and "Etag" in response.headers + and response.headers["Etag"] + in parse_etags(request.headers["If-None-Match"]) + ): not_modified = HttpResponse(status=304) not_modified.headers["Etag"] = response.headers["Etag"] # TODO: Cache-Control and Expires? From 5b781824a610e5e2d4c01907e8c3b0b9d48087c7 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Fri, 19 Jul 2024 11:39:00 +0200 Subject: [PATCH 9/9] set cache-control and expires header on 304 response --- wagtailcache/cache.py | 30 ++++++++++++++++-------------- wagtailcache/settings.py | 1 + 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 44664e3..0cb5f3c 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -31,7 +31,6 @@ from wagtailcache.settings import wagtailcache_settings - logger = logging.getLogger("wagtail-cache") @@ -144,11 +143,11 @@ def _chop_response_vary(r: WSGIRequest, s: HttpResponse) -> HttpResponse: and s.has_header("Vary") and has_vary_header(s, "Cookie") and not ( - settings.CSRF_COOKIE_NAME in s.cookies - or settings.CSRF_COOKIE_NAME in r.COOKIES - or settings.SESSION_COOKIE_NAME in s.cookies - or settings.SESSION_COOKIE_NAME in r.COOKIES - ) + settings.CSRF_COOKIE_NAME in s.cookies + or settings.CSRF_COOKIE_NAME in r.COOKIES + or settings.SESSION_COOKIE_NAME in s.cookies + or settings.SESSION_COOKIE_NAME in r.COOKIES + ) ): _delete_vary_cookie(s) return s @@ -238,14 +237,16 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: # Hit. Return cached response or Not Modified. setattr(request, "_wagtailcache_update", False) if ( - "If-None-Match" in request.headers + wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS + and "If-None-Match" in request.headers and "Etag" in response.headers and response.headers["Etag"] in parse_etags(request.headers["If-None-Match"]) ): not_modified = HttpResponse(status=304) not_modified.headers["Etag"] = response.headers["Etag"] - # TODO: Cache-Control and Expires? + not_modified.headers["Cache-Control"] = response.headers["Cache-Control"] + not_modified.headers["Expires"] = response.headers["Expires"] return not_modified return response @@ -308,10 +309,10 @@ def process_response( and response.status_code in (200, 301, 302, 304, 404) and not response.streaming and not ( - not request.COOKIES - and response.cookies - and has_vary_header(response, "Cookie") - ) + not request.COOKIES + and response.cookies + and has_vary_header(response, "Cookie") + ) ) # Allow the user to override our caching decision. for fn in hooks.get_hooks("is_response_cacheable"): @@ -337,7 +338,8 @@ def process_response( patch_cache_control(response, no_cache=True) else: patch_response_headers(response, max_age) - patch_cache_control(response, must_revalidate=True) + if wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS: + patch_cache_control(response, must_revalidate=True) if timeout != 0: try: @@ -359,7 +361,7 @@ def process_response( self._wagcache.set("keyring", keyring) def set_etag(r): - if "Etag" not in r.headers: + if wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS and "Etag" not in r.headers: set_response_etag(r) if isinstance(response, SimpleTemplateResponse): diff --git a/wagtailcache/settings.py b/wagtailcache/settings.py index 380ddba..fda096c 100644 --- a/wagtailcache/settings.py +++ b/wagtailcache/settings.py @@ -13,6 +13,7 @@ class _DefaultSettings: WAGTAIL_CACHE_HEADER = "X-Wagtail-Cache" WAGTAIL_CACHE_IGNORE_COOKIES = True WAGTAIL_CACHE_MAX_AGE = 5 * 60 + WAGTAIL_CACHE_USE_ETAGS = False WAGTAIL_CACHE_IGNORE_QS = [ r"^_bta_.*$", # Bronto r"^_ga$", # Google Analytics