Remove CDN Cache-Control headers; fix cross-language sort bug
Mizan's protocol layers (origin Redis cache, Edge Worker) handle caching
autonomously. The origin emits Cache-Control: no-store on ALL responses —
browsers and non-Mizan intermediaries must not cache. The Edge Worker
controls CDN caching via cf object, independent of origin headers.
Also fixes:
- TS localeCompare → byte-order sort (localeCompare is locale-sensitive,
would produce different HMAC keys for non-ASCII params vs Python)
- Python cache_purge: empty {} params no longer treated as falsy
(was inconsistent with JS where {} is truthy)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,7 +121,7 @@ def cache_purge(
|
||||
Broad purge (no params): scans by key prefix "ctx:{context}:*".
|
||||
This is a rare operation (Tier 3 fallback in invalidation).
|
||||
"""
|
||||
if params and secret:
|
||||
if params is not None and len(params) > 0 and secret:
|
||||
key = derive_cache_key(secret, context, params, user_id, rev)
|
||||
return 1 if backend.delete(key) else 0
|
||||
else:
|
||||
|
||||
@@ -874,10 +874,7 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
||||
)
|
||||
if cached is not None:
|
||||
response = HttpResponse(cached, content_type="application/json")
|
||||
if isinstance(effective_cache, int):
|
||||
response["Cache-Control"] = f"public, max-age=0, s-maxage={effective_cache}"
|
||||
else:
|
||||
response["Cache-Control"] = "public, max-age=0, s-maxage=31536000"
|
||||
response["Cache-Control"] = "no-store"
|
||||
response["X-Mizan-Cache"] = "HIT"
|
||||
return response
|
||||
except Exception:
|
||||
@@ -902,13 +899,9 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
||||
# Deterministic JSON (sorted keys) for consistent cache keys
|
||||
response = JsonResponse(result.data, json_dumps_params={"sort_keys": True})
|
||||
|
||||
# CDN-ready headers based on effective cache policy
|
||||
if effective_cache is False:
|
||||
response["Cache-Control"] = "no-store"
|
||||
elif isinstance(effective_cache, int):
|
||||
response["Cache-Control"] = f"public, max-age=0, s-maxage={effective_cache}"
|
||||
else:
|
||||
response["Cache-Control"] = "public, max-age=0, s-maxage=31536000"
|
||||
# Mizan's protocol layers handle caching (origin Redis, Edge Worker).
|
||||
# The browser and non-Mizan intermediaries must not cache.
|
||||
response["Cache-Control"] = "no-store"
|
||||
|
||||
# Store in origin-side cache (skip if cache=False)
|
||||
if use_cache:
|
||||
|
||||
@@ -1019,9 +1019,8 @@ class ServerDrivenInvalidationTests(TestCase):
|
||||
self.assertIn("team_info", data)
|
||||
self.assertEqual(data["team_info"]["name"], "team_3")
|
||||
|
||||
# CDN-ready headers
|
||||
self.assertIn("public", response["Cache-Control"])
|
||||
self.assertIn("s-maxage", response["Cache-Control"])
|
||||
# Mizan handles caching via its protocol; origin emits no-store
|
||||
self.assertEqual(response["Cache-Control"], "no-store")
|
||||
|
||||
def test_context_error_not_cached(self):
|
||||
"""Context fetch errors must not be cached."""
|
||||
@@ -1785,9 +1784,8 @@ class HTTPIntegrationTests(TestCase):
|
||||
self.assertEqual(data["user_profile"]["name"], "user_5")
|
||||
self.assertEqual(data["user_orders"]["count"], 50)
|
||||
|
||||
# CDN headers
|
||||
self.assertIn("public", response["Cache-Control"])
|
||||
self.assertIn("s-maxage", response["Cache-Control"])
|
||||
# Mizan handles caching; origin emits no-store
|
||||
self.assertEqual(response["Cache-Control"], "no-store")
|
||||
|
||||
def test_context_fetch_string_to_int_coercion(self):
|
||||
"""Query params arrive as strings. Pydantic must coerce to int."""
|
||||
@@ -2142,16 +2140,10 @@ class EdgeCompatibilityTests(TestCase):
|
||||
|
||||
# ── Cache-Control correctness ───────────────────────────────────────────
|
||||
|
||||
def test_context_get_is_cacheable(self):
|
||||
"""Context GET has Cache-Control that allows CDN caching."""
|
||||
def test_context_get_no_store(self):
|
||||
"""Context GET emits no-store. Mizan's protocol layers handle caching."""
|
||||
response = self.client.get("/api/mizan/ctx/user/?user_id=5")
|
||||
|
||||
cc = response["Cache-Control"]
|
||||
self.assertIn("public", cc)
|
||||
self.assertIn("s-maxage", cc)
|
||||
# Must NOT have no-store or private
|
||||
self.assertNotIn("no-store", cc)
|
||||
self.assertNotIn("private", cc)
|
||||
self.assertEqual(response["Cache-Control"], "no-store")
|
||||
|
||||
def test_mutation_post_not_cacheable(self):
|
||||
"""Mutation POST has no-store. CDN must never cache mutations."""
|
||||
@@ -3196,8 +3188,8 @@ class CachePolicyIntegrationTests(TestCase):
|
||||
def tearDown(self):
|
||||
clear_registry()
|
||||
|
||||
def test_cache_int_sets_smaxage(self):
|
||||
"""Context with cache=60 emits s-maxage=60."""
|
||||
def test_cache_int_still_no_store_header(self):
|
||||
"""cache=60 affects origin Redis TTL, but HTTP header is always no-store."""
|
||||
Ctx = ReactContext("trending")
|
||||
|
||||
@client(context=Ctx, cache=60)
|
||||
@@ -3206,10 +3198,9 @@ class CachePolicyIntegrationTests(TestCase):
|
||||
|
||||
register(trending, "trending")
|
||||
response = self.client.get("/api/mizan/ctx/trending/")
|
||||
self.assertIn("s-maxage=60", response["Cache-Control"])
|
||||
self.assertNotIn("31536000", response["Cache-Control"])
|
||||
self.assertEqual(response["Cache-Control"], "no-store")
|
||||
|
||||
def test_cache_false_sets_nostore(self):
|
||||
def test_cache_false_no_store(self):
|
||||
"""Context with cache=False emits no-store."""
|
||||
Ctx = ReactContext("random")
|
||||
|
||||
@@ -3221,23 +3212,6 @@ class CachePolicyIntegrationTests(TestCase):
|
||||
response = self.client.get("/api/mizan/ctx/random/")
|
||||
self.assertEqual(response["Cache-Control"], "no-store")
|
||||
|
||||
def test_effective_cache_is_minimum(self):
|
||||
"""Context with mixed TTLs uses the minimum."""
|
||||
Ctx = ReactContext("mixed")
|
||||
|
||||
@client(context=Ctx, cache=300)
|
||||
def slow_fn(request: HttpRequest) -> dict:
|
||||
return {"slow": True}
|
||||
|
||||
@client(context=Ctx, cache=60)
|
||||
def fast_fn(request: HttpRequest) -> dict:
|
||||
return {"fast": True}
|
||||
|
||||
register(slow_fn, "slow_fn")
|
||||
register(fast_fn, "fast_fn")
|
||||
response = self.client.get("/api/mizan/ctx/mixed/")
|
||||
self.assertIn("s-maxage=60", response["Cache-Control"])
|
||||
|
||||
def test_effective_rev_is_maximum(self):
|
||||
"""Context with mixed revs uses the maximum for cache key."""
|
||||
from mizan.cache.keys import derive_cache_key
|
||||
|
||||
Reference in New Issue
Block a user