From a2ef792f0f2eb7ef2548e1b907c05a53afdb966f Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 09:20:14 -0700 Subject: [PATCH] fix(sim): revalidate /media instead of pinning it immutable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `immutable, max-age=1y` media header pinned stale footage in the browser for a year, so a re-sourced or re-cropped clip (e.g. the de-captioned cosmos) never showed without a manual cache clear. Switch /media to `no-cache` (store, but revalidate every load): instant altitude swaps already come from the in-memory blob preload, so the HTTP cache only matters on (re)load — where a conditional request is a cheap 304 when unchanged but picks up edited clips immediately. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/app.py | 12 +++++++----- tests/test_simulator_api.py | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/simulator/app.py b/simulator/app.py index 0266abb..7ebe72d 100644 --- a/simulator/app.py +++ b/simulator/app.py @@ -237,13 +237,15 @@ def create_app(manifest_path: Optional[Path] = None) -> FastAPI: async def _cache_policy(request, call_next): # Dev preview server: never let a browser serve a stale app.js/style.css — # by-eye iteration needs a plain refresh to always get the latest assets. - # Media is the exception: the clips are static and large, so they must be - # browser-cacheable. The renderer also preloads them into in-memory blob - # URLs (static/app.js), but a cacheable header keeps the preload fetch and - # any fallback off the network on repeat loads — instant altitude swaps. + # Media uses `no-cache` (store, but REVALIDATE every load): instant altitude + # swaps already come from the in-memory blob preload (static/app.js), so the + # HTTP cache only matters on (re)load — where a conditional request returns a + # cheap 304 when unchanged, yet picks up a re-sourced/re-cropped clip + # immediately. `immutable` was wrong here: it pinned stale footage for a year + # so edited clips never showed without a manual cache clear. response = await call_next(request) if request.url.path.startswith("/media/"): - response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + response.headers["Cache-Control"] = "no-cache" else: response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return response diff --git a/tests/test_simulator_api.py b/tests/test_simulator_api.py index 0fcc4d8..b3c5669 100644 --- a/tests/test_simulator_api.py +++ b/tests/test_simulator_api.py @@ -113,8 +113,11 @@ def test_media_is_cacheable(tmp_path, monkeypatch): resp = client.get("/media/cosmos/base.mp4") assert resp.status_code == 200 cc = resp.headers.get("cache-control", "") + # Stored but revalidated: not no-store (so the blob preload can cache it), and + # not pinned immutable (so a re-sourced/re-cropped clip shows on a plain reload). assert "no-store" not in cc - assert "max-age" in cc + assert "immutable" not in cc + assert "no-cache" in cc @pytest.fixture