Skip to content

Audit other SPAs behind nginx-router for the same weak-default-caching bug on hashed assets #86

Description

@man4ish

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions