aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:16 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:16 +0200
commitaa63cae5f004fc4d01471561404d59360784b784 (patch)
tree0599e517f3501435b345a731e839b0f977df0a1f /packages/meshbay-hub
parent3b2d03a919e56d14f8097844b97b844ddf526cd6 (diff)
downloadmeshbay-aa63cae5f004fc4d01471561404d59360784b784.tar.gz
fix(hub): revalidate static assets so a split SPA version cannot load
The SPA became a module graph that has to agree with itself: app.js imports i18n.js, which imports a catalogue from locales/. Serving one of them from cache while fetching another is not a stale page, it is a broken one, and both directions were reproduced: - a browser holding the previous i18n.js fails to *link* the new app.js ("does not provide an export named 'initLocale'"). That happens before any code runs, so the fallback in the boot cannot catch it — blank page. - the reverse pairing links and renders, but nothing calls initLocale(), so every string comes out as its own key: nav.logout, group.tab_files. Starlette sends only etag and last-modified, and with no explicit freshness a browser is entitled to guess one — roughly 10% of the age since last-modified, which is comfortably long enough to catch a deploy. Caddy is a plain reverse proxy and adds nothing. `no-cache` does not disable caching; it requires a conditional request, which the existing ETag answers with a 304 and no body. Measured: 304, 0 bytes, on every static path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/app.py24
1 files changed, 23 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py
index 668ae24..6240785 100644
--- a/packages/meshbay-hub/src/meshbay_hub/app.py
+++ b/packages/meshbay-hub/src/meshbay_hub/app.py
@@ -123,6 +123,28 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI:
app.include_router(webapp_router)
from starlette.staticfiles import StaticFiles
- app.mount("/", StaticFiles(directory=STATIC_DIR), name="static")
+
+ class RevalidatingStatics(StaticFiles):
+ """Static assets that must be revalidated, never served blind from cache.
+
+ The SPA is a module graph that has to agree with itself: `app.js` imports
+ `i18n.js`, which imports a catalogue from `locales/`. Serving one of them
+ from cache while fetching another is not a stale page, it is a broken
+ one — a browser holding the previous `i18n.js` fails to link the new
+ `app.js` ("does not provide an export named 'initLocale'") before any
+ code runs, and the reverse pairing renders every string as its own key.
+
+ Starlette sends only `etag` and `last-modified`, and with no explicit
+ freshness a browser is entitled to guess one. `no-cache` does not
+ disable caching: it requires a conditional request, which the existing
+ ETag answers with a 304 and no body.
+ """
+
+ async def get_response(self, path, scope):
+ response = await super().get_response(path, scope)
+ response.headers.setdefault("Cache-Control", "no-cache")
+ return response
+
+ app.mount("/", RevalidatingStatics(directory=STATIC_DIR), name="static")
return app