Background
2026-09-11 incident: RAG / Lit AI presented a blank page with the console error
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of 'text/html' — identical symptom to the Sep 4 nginx
prefix-stripping bug, but not a regression of it. Root cause was a stale browser cache:
ragbio's FastAPI StaticFiles mount sends no Cache-Control at all for its content-hashed
/assets/*.js|css bundles, so Cloudflare filled in its own ambiguous default
(max-age=14400, no immutable). Whether a given client got a fresh or stale copy came down
to revalidation timing rather than anything deterministic — and because the filenames are
content-hashed, a cache entry holding a wrong body (from a past routing bug) is never evicted
by any redeploy.
Full writeup: ~/rag-blank-page-browser-cache-report-2026-09-11.md (local to the investigating
machine, not in-repo — referenced from docker/nginx-router.conf's /_svc/rag/assets/
location comment).
Fix already applied (rag only)
docker/nginx-router.conf's new location ^~ /_svc/rag/assets/ block now serves
Cache-Control: public, max-age=31536000, immutable for rag's hashed assets — safe because
the filenames are content-addressed and genuinely never change for a given URL. Scoped to
assets/ only; index.html deliberately stays uncached since it names the hashes to fetch.
What's left
grep -c "immutable\|31536000" docker/nginx-router.conf was 0 before the rag fix — this
pattern didn't exist anywhere else in the file. Every other SPA proxied through nginx-router
that serves content-hashed build output is presumably running on the same Cloudflare default
and exposed to the same failure class:
/_svc/modelregistry (model-registry-ui)
/_svc/devhub
- control-center's own frontend
- workbench plugin SPAs (
/_svc/workbench/plugins/*)
For each: confirm the asset filenames are actually content-hashed (Vite/webpack build output —
not true for every service, e.g. hand-rolled static files shouldn't get immutable), then add
the same location ^~ .../assets/ { add_header Cache-Control "public, max-age=31536000, immutable" always; } pattern used for rag, scoped only to the hashed-asset sub-path, never to
the SPA's own index.html/entry document.
Not urgent — the bad-cache window only opens if one of these routes actually breaks — but
closes the failure mode class-wide.
Background
2026-09-11 incident: RAG / Lit AI presented a blank page with the console error
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of 'text/html'— identical symptom to the Sep 4 nginxprefix-stripping bug, but not a regression of it. Root cause was a stale browser cache:
ragbio's FastAPI
StaticFilesmount sends noCache-Controlat all for its content-hashed/assets/*.js|cssbundles, so Cloudflare filled in its own ambiguous default(
max-age=14400, noimmutable). Whether a given client got a fresh or stale copy came downto revalidation timing rather than anything deterministic — and because the filenames are
content-hashed, a cache entry holding a wrong body (from a past routing bug) is never evicted
by any redeploy.
Full writeup:
~/rag-blank-page-browser-cache-report-2026-09-11.md(local to the investigatingmachine, not in-repo — referenced from
docker/nginx-router.conf's/_svc/rag/assets/location comment).
Fix already applied (rag only)
docker/nginx-router.conf's newlocation ^~ /_svc/rag/assets/block now servesCache-Control: public, max-age=31536000, immutablefor rag's hashed assets — safe becausethe filenames are content-addressed and genuinely never change for a given URL. Scoped to
assets/only;index.htmldeliberately stays uncached since it names the hashes to fetch.What's left
grep -c "immutable\|31536000" docker/nginx-router.confwas 0 before the rag fix — thispattern didn't exist anywhere else in the file. Every other SPA proxied through nginx-router
that serves content-hashed build output is presumably running on the same Cloudflare default
and exposed to the same failure class:
/_svc/modelregistry(model-registry-ui)/_svc/devhub/_svc/workbench/plugins/*)For each: confirm the asset filenames are actually content-hashed (Vite/webpack build output —
not true for every service, e.g. hand-rolled static files shouldn't get
immutable), then addthe same
location ^~ .../assets/ { add_header Cache-Control "public, max-age=31536000, immutable" always; }pattern used for rag, scoped only to the hashed-asset sub-path, never tothe SPA's own
index.html/entry document.Not urgent — the bad-cache window only opens if one of these routes actually breaks — but
closes the failure mode class-wide.