Skip to content

Fix server-side cache to work with conditional GET#608

Open
skaphan wants to merge 6 commits intoartoonie:mainfrom
skaphan:fix-server-cache
Open

Fix server-side cache to work with conditional GET#608
skaphan wants to merge 6 commits intoartoonie:mainfrom
skaphan:fix-server-cache

Conversation

@skaphan
Copy link

@skaphan skaphan commented Mar 6, 2026

Summary

  • Removes max_age=0 from patch_cache_control in ConditionalGetMixin -- this was preventing UpdateCacheMiddleware from storing responses server-side, since it checks get_max_age(response) and skips caching when the result is 0.
  • Replaces cache.clear() with per-URL cache purging in cloudflare.py -- uses get_cache_key() with synthetic HttpRequest objects to surgically invalidate only the affected visualization URLs from the file-based cache.
  • Removes DISABLE_CACHE toggle from settings -- the file-based cache is now always enabled and works correctly with conditional GET.
  • Removes vary_on_headers(increment) -- this was a no-op since no client ever sends that header.

How it works

  1. ConditionalGetMixin sets Cache-Control: no-cache (browsers revalidate on every request)
  2. UpdateCacheMiddleware stores the rendered response in Django file-based cache (now works because max_age is not 0)
  3. FetchFromCacheMiddleware serves cached responses on subsequent requests
  4. ConditionalGetMiddleware compares Last-Modified vs If-Modified-Since and returns 304 when content has not changed
  5. On model update, CloudflareAPI.purge_vis_cache() clears both Cloudflare CDN and the specific Django cache entries

Builds on #594 (proper cache control).

skaphan and others added 5 commits February 24, 2026 13:29
Add updated_at field to JsonConfig model, use ConditionalGetMixin for
all visualization views, and short-circuit 304 responses in
VisualizeEmbedded before expensive computation.
Add proper cache control for embedded visualizations
- Add pylint disable for too-few-public-methods (it is a mixin)
- Add docstring to get() method
- Rename last_modified/if_modified_since to camelCase

Co-Authored-By: Claude Opus 4.6
- Remove max_age=0 from ConditionalGetMixin so UpdateCacheMiddleware
  can store rendered responses in the file cache. Browsers still
  revalidate via Cache-Control: no-cache.
- Replace cache.clear() sledgehammer in cloudflare.py with per-URL
  cache purging using get_cache_key — only the updated slug's pages
  are evicted from Django's file cache.
- Remove vary_on_headers('increment') — was a no-op (no client sends
  the header).
- Remove DISABLE_CACHE env toggle — no longer needed.
- ConditionalGetMiddleware (already in middleware stack) handles 304
  responses for cached pages using the Last-Modified header.

Co-Authored-By: Claude Opus 4.6
Copy link
Owner

@artoonie artoonie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This looks pretty close to ready. I recommend reverting the changes to purging django cache and DISABLE_CACHE for now, as that would be easier than fixing them.

request.path = path
request.META['QUERY_STRING'] = ''
request.META['SERVER_NAME'] = 'localhost'
request.META['SERVER_PORT'] = '80'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will work in development but not in production -- we are missing HTTP_HOST (which could be rcvis.com or www.rcvis.com), url scheme (https), and maybe others.

I think the only way to get this to work is to store a local map of slugs to cache IDs and to use that to clear the cache -- manually building the cache key is brittle.


AWS_DEFAULT_ACL = None

if os.environ.get('DISABLE_CACHE') != 'True':
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If removing this, we should also remove it from infra/.env.template.

However -- during development, it's still useful to be able to disable cache. Not all cache invalidations come from PATCHes. If you update an HTML or JS file locally, you want to be able to refresh the page without restarting the server. It doesn't always work (because JS files are minified and templates are compiled), but there are cases when it does.

print('API user skaphan already exists')
"

echo "Database reset complete."
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel comfortable committing this to the RCVis mainline -- a database deletion script feels like a high-risk script to leave in production.

@skaphan
Copy link
Author

skaphan commented Mar 6, 2026 via email

- _purge_django_cache now uses Site.objects.get_current().domain to
  construct requests with the correct HTTP_HOST and HTTPS scheme,
  matching how UpdateCacheMiddleware stores cache keys in production.
  Tries both primary domain and www. variant.
- Extracted _make_cache_request helper for constructing synthetic requests.
- Restored DISABLE_CACHE env var toggle for development convenience.
- Removed reset-db.sh (not appropriate for mainline).
- Added three tests for django cache purging: basic path, query string,
  and www. variant coverage.

Co-Authored-By: Claude Opus 4.6
@skaphan skaphan force-pushed the fix-server-cache branch from f69968a to fd01651 Compare March 6, 2026 18:46
@skaphan
Copy link
Author

skaphan commented Mar 6, 2026

Thanks for the review! I've pushed changes addressing all three points:

Cache purging: You're right that the synthetic request was missing production-level headers. I've fixed _purge_django_cache to use Site.objects.get_current().domain for the HTTP_HOST (the same source get_absolute_paths_for already uses for Cloudflare purges) and set wsgi.url_scheme to https. It also tries both the primary domain and the www. variant, mirroring the Cloudflare purge logic. I extracted a _make_cache_request helper to keep it testable and added three tests covering basic paths, query strings, and the www variant.

Without per-URL purging, the cache would serve stale responses after a PATCH — FetchFromCacheMiddleware returns the old cached page before ConditionalGetMixin ever runs, so the Last-Modified check never happens. If you'd prefer to revert to cache.clear() as a simpler fallback, I'm happy to do that instead.

DISABLE_CACHE: Restored.

reset-db.sh: Removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants