Skip to content

fix: robust cache busting on deployment#80

Merged
munisp merged 2 commits into
mainfrom
devin/1781876483-cache-busting
Jun 19, 2026
Merged

fix: robust cache busting on deployment#80
munisp merged 2 commits into
mainfrom
devin/1781876483-cache-busting

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

After a deploy, browsers could serve stale index.html (and therefore stale <script> tags pointing to old content-hashed chunks that no longer exist on the server), causing white-screen errors. This PR adds a 4-layer cache-busting strategy:

Layer 1 — Express server (server/_core/vite.ts):

/assets/*  → Cache-Control: public, immutable; max-age=1y   (Vite content-hash)
/sw.js     → Cache-Control: no-cache, no-store
*.html     → Cache-Control: no-cache, no-store, must-revalidate + Pragma + Expires: 0
/*  (SPA)  → same as *.html (fallback for client-side routes)

Key subtlety: express.static intercepts /index.html before the SPA fallback fires, so setHeaders must handle .html files explicitly — without this, index.html would be served with max-age=3600.

Layer 2 — Service worker (client/public/sw.js):

  • Bumped to ndsep-v3; on activate, purges ALL caches not matching current version
  • Navigation requests are always network-first (never serves stale index.html)
  • Posts CACHE_BUSTED message to all open tabs → triggers reload
  • Accepts CLEAR_CACHES message from client for manual cache wipe
  • Checks for SW updates every 60s

Layer 3 — HTML meta tags (client/index.html):

  • <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
  • <meta http-equiv="Pragma" content="no-cache">
  • <meta http-equiv="Expires" content="0">

Layer 4 — Nginx (infra/nginx/nginx.conf + conf.d/locations.conf):

  • Split the single static-asset rule into 5 location blocks:
    • /assets/ → immutable 1y (hashed Vite chunks)
    • = /sw.js → no-cache, no-store (must always be fresh)
    • = /manifest.json → no-cache, must-revalidate
    • \.(png|jpg|...)$ → 1h with must-revalidate
    • / (HTML fallback) → no-cache, no-store, must-revalidate
  • Applied to both HTTPS server block and WAF backend (locations.conf)

Link to Devin session: https://app.devin.ai/sessions/7b19b09de740454faef61082df9c86da
Requested by: @munisp

- Express: index.html served with no-cache/no-store headers; /assets/ immutable 1y
- Service worker: version-based cache purge on activate; CACHE_BUSTED message to clients
- HTML: meta http-equiv Cache-Control/Pragma/Expires tags prevent proxy caching
- Nginx: split /assets/ (immutable) from HTML (no-cache); sw.js/manifest.json dedicated rules
- Client: SW update listener auto-reloads tabs when new version deploys

Co-Authored-By: Patrick Munis <pmunis@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from Patrick

NDSEP continued session id 638573251e5f4e859a5f3b205afec3cd

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

express.static intercepts / → index.html before the SPA fallback,
so the setHeaders callback must also handle .html files to prevent
1-hour caching of the HTML entry point.

Co-Authored-By: Patrick Munis <pmunis@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Cache Busting Test Results

All 7 tests passed. Tested by building the production bundle and serving it via a minimal Express server that replicates serveStatic().

Express Response Header Tests
Test URL Expected Cache-Control Actual Result
Root / (index.html) GET / no-cache, no-store, must-revalidate no-cache, no-store, must-revalidate + Pragma: no-cache + Expires: 0 PASS
SPA fallback /dashboard GET /dashboard no-cache, no-store, must-revalidate no-cache, no-store, must-revalidate + Pragma: no-cache + Expires: 0 PASS
Hashed JS asset GET /assets/AIAssistant-DsCkdCr1.js public, max-age=31536000, immutable public, max-age=31536000, immutable PASS
Service worker GET /sw.js no-cache, no-store, must-revalidate no-cache, no-store, must-revalidate PASS
registerSW.js GET /registerSW.js no-cache, no-store, must-revalidate no-cache, no-store, must-revalidate PASS
Built HTML Verification
  • dist/public/index.html contains <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">PASS
  • dist/public/index.html contains <meta http-equiv="Pragma" content="no-cache">PASS
  • dist/public/index.html contains <meta http-equiv="Expires" content="0">PASS
  • dist/public/index.html contains CACHE_BUSTED listener (2 occurrences) — PASS
  • dist/public/index.html contains reg.update() (60s SW update check) — PASS
  • dist/public/index.html contains navigator.serviceWorker.register('/sw.js')PASS
Nginx Config Verification

Both infra/nginx/nginx.conf and infra/nginx/conf.d/locations.conf contain 5 location blocks:

  • /assets/expires 1y + Cache-Control "public, immutable"PASS
  • = /sw.jsCache-Control "no-cache, no-store, must-revalidate"PASS
  • = /manifest.jsonCache-Control "no-cache, must-revalidate"PASS
  • ~* \.(png|jpg|...)$expires 1hPASS
  • /Cache-Control "no-cache, no-store, must-revalidate" alwaysPASS
Bug Found & Fixed During Testing

Issue: express.static intercepts GET / → serves index.html with max-age=3600 (1 hour cache) before the SPA fallback app.use("*", ...) can set no-cache headers.

Fix (commit 402197d): Added .html handling to the setHeaders callback in express.static, so index.html gets no-cache, no-store, must-revalidate regardless of whether it's served by the static middleware or the SPA fallback.

Devin session

@munisp munisp merged commit 801d78e into main Jun 19, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant