diff --git a/bridge.py b/bridge.py index 465d52d..6cd241b 100755 --- a/bridge.py +++ b/bridge.py @@ -96,8 +96,17 @@ async def relay(request: web.Request) -> web.WebSocketResponse: # The client loads no inline script, no inline style and no foreign origin, so a # strict policy costs nothing. form-action stays 'self' rather than 'none' so the # compose dialog's method="dialog" form is untouched. +# +# Nix normalizes every built file's mtime to the same fixed epoch for +# reproducibility, so Last-Modified is identical across every deploy — with no +# explicit Cache-Control, a browser's heuristic freshness calculation (based on +# that ~56-year-old timestamp) can treat a page as fresh indefinitely and never +# revalidate again, silently pinning a visitor to whatever JS they first +# loaded across every future deploy. no-cache forces a conditional GET each +# time rather than disabling caching outright — the ETag aiohttp already sends +# still turns an unchanged file into a bodyless 304. @web.middleware -async def csp(request: web.Request, handler): +async def headers(request: web.Request, handler): response = await handler(request) response.headers.setdefault("Content-Security-Policy", "; ".join([ "default-src 'self'", @@ -107,6 +116,7 @@ async def csp(request: web.Request, handler): "object-src 'none'", "frame-ancestors 'none'", ])) + response.headers.setdefault("Cache-Control", "no-cache") return response @@ -128,7 +138,7 @@ def main() -> int: args = parser.parse_args() logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") - app = web.Application(middlewares=[csp]) + app = web.Application(middlewares=[headers]) app["web_dir"], app["relay_ports"] = args.dir, set(args.allow_ports) app["allow_origins"] = set(args.allow_origins) app.router.add_get("/", index)