chat.lotusguild.org serves the app shell for /, /home/… and /login/… without Cache-Control; deploys delete the old hashed assets #23

Open
opened 2026-10-05 01:47:04 -04:00 by jared · 0 comments
Owner

cinny/nginx.conf means to never cache HTML (the "Never cache HTML or JSON (index.html, config.json, manifest.json)" block), and the service worker relies on it (src/sw.ts, "DEPLOY-SAFETY INVARIANTS: index.html / navigations are NEVER precached … a new deploy is picked up immediately"). But SPA URLs reach index.html through location / { … rewrite ^(.+)$ /index.html break; }. With break, nginx serves the file inside location /, which sets no cache headers. The \.(json|html)$ location never sees the request.

Live headers (2026-10-05):

GET /                                   200  last-modified: Sat, 03 Oct 2026 04:53:00 GMT   (no Cache-Control, no Expires)
GET /home/                              200  last-modified: …                              (no Cache-Control, no Expires)
GET /login/matrix.lotusguild.org/       200  last-modified: …                              (no Cache-Control, no Expires)
GET /index.html                         200  cache-control: no-cache / no-cache, no-store, must-revalidate
GET /assets/index-DL_PmJL1.js           200  cache-control: max-age=31536000 / public, immutable

With only Last-Modified, browsers apply heuristic freshness, typically 10 % of the document's age. A shell last modified two days ago can be reused without asking for about 5 hours.

lotus_deploy.sh:163 deploys with rsync -a --delete … dist/ "$WEBROOT/", which removes the previous build's hashed assets. A browser that reuses a heuristically fresh shell after a deploy therefore requests assets/index-<old>.js, which 404s, and the user gets a blank or broken app until the cached shell expires. Users whose service worker still has the old precache are spared; first-time or SW-less visitors (private windows, Firefox with storage blocked, cleared SW) are not. Opening the PWA or a bookmarked /home/ link is the typical trigger, while a manual reload revalidates.

Fix (any one):

  • Add the no-cache headers in location / too (add_header Cache-Control "no-cache" always;, plus the security headers include, since add_header doesn't inherit). Or route the rewrite to a named location that has them.
  • Keep the previous build's assets/ for a day after a deploy (drop --delete for assets/, prune old files on the next run).

Also noticed:

  • Responses carry two Strict-Transport-Security headers (max-age=63072000; includeSubDomains; preload and max-age=63072000; preload). Browsers honour only the first; keep one, probably at a single layer (snippet or NPM, not both).
  • Two Cache-Control headers (expires -1 already emits Cache-Control: no-cache, then add_header adds no-cache, no-store, must-revalidate). Harmless, but dropping expires -1 keeps it to one.
  • sw.js, config.json, manifest.json, the call page (call.chat.lotusguild.org/public/element-call/index.html, frame-ancestors https://chat.lotusguild.org) and hashed assets (1 y, immutable) are all set correctly.

Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.

`cinny/nginx.conf` means to never cache HTML (the "Never cache HTML or JSON (index.html, config.json, manifest.json)" block), and the service worker relies on it (`src/sw.ts`, "DEPLOY-SAFETY INVARIANTS: index.html / navigations are NEVER precached … a new deploy is picked up immediately"). But SPA URLs reach `index.html` through `location / { … rewrite ^(.+)$ /index.html break; }`. With `break`, nginx serves the file **inside `location /`**, which sets no cache headers. The `\.(json|html)$` location never sees the request. **Live headers (2026-10-05):** ``` GET / 200 last-modified: Sat, 03 Oct 2026 04:53:00 GMT (no Cache-Control, no Expires) GET /home/ 200 last-modified: … (no Cache-Control, no Expires) GET /login/matrix.lotusguild.org/ 200 last-modified: … (no Cache-Control, no Expires) GET /index.html 200 cache-control: no-cache / no-cache, no-store, must-revalidate GET /assets/index-DL_PmJL1.js 200 cache-control: max-age=31536000 / public, immutable ``` With only `Last-Modified`, browsers apply **heuristic freshness**, typically 10 % of the document's age. A shell last modified two days ago can be reused without asking for about 5 hours. `lotus_deploy.sh:163` deploys with `rsync -a --delete … dist/ "$WEBROOT/"`, which **removes the previous build's hashed assets**. A browser that reuses a heuristically fresh shell after a deploy therefore requests `assets/index-<old>.js`, which 404s, and the user gets a blank or broken app until the cached shell expires. Users whose service worker still has the old precache are spared; first-time or SW-less visitors (private windows, Firefox with storage blocked, cleared SW) are not. Opening the PWA or a bookmarked `/home/` link is the typical trigger, while a manual reload revalidates. **Fix (any one):** - Add the no-cache headers in `location /` too (`add_header Cache-Control "no-cache" always;`, plus the security headers include, since `add_header` doesn't inherit). Or route the rewrite to a named location that has them. - Keep the previous build's `assets/` for a day after a deploy (drop `--delete` for `assets/`, prune old files on the next run). **Also noticed:** - Responses carry **two `Strict-Transport-Security` headers** (`max-age=63072000; includeSubDomains; preload` and `max-age=63072000; preload`). Browsers honour only the first; keep one, probably at a single layer (snippet or NPM, not both). - **Two `Cache-Control` headers** (`expires -1` already emits `Cache-Control: no-cache`, then `add_header` adds `no-cache, no-store, must-revalidate`). Harmless, but dropping `expires -1` keeps it to one. - `sw.js`, `config.json`, `manifest.json`, the call page (`call.chat.lotusguild.org/public/element-call/index.html`, `frame-ancestors https://chat.lotusguild.org`) and hashed assets (1 y, immutable) are all set correctly. --- _Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
jared added the infrapriority: mediumbug labels 2026-10-05 01:47:04 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/matrix#23