Keep PWA builds coherent by bypassing the HTTP cache

The service worker populated a new build's cache with plain addAll(),
which the browser may satisfy from its HTTP cache — and static assets
were served without Cache-Control, so Safari's heuristic caching could
hold app.js for days. Together that could install a mixed build: a
fresh index.html whose buttons reference listeners a stale app.js never
registers.

Install now fetches assets with cache: "reload", and the server marks
all static assets no-cache (revalidation is a cheap 304; fast/offline
loads are the SW cache's job anyway).
This commit is contained in:
Alexander Heldt
2026-07-12 17:13:13 +00:00
parent ced415c3a5
commit 2b4731185c
2 changed files with 12 additions and 5 deletions
+5 -4
View File
@@ -806,12 +806,13 @@ func main() {
serveSW(w, *staticDir, swVer)
return
}
// PWA: manifest.json must revalidate so updates propagate.
if r.URL.Path == "/manifest.json" {
w.Header().Set("Cache-Control", "no-cache")
}
// SPA fallback: unknown paths -> index.html (so deep links work).
if !strings.HasPrefix(r.URL.Path, "/api/") {
// All static assets revalidate on every request (cheap 304s via
// Last-Modified). Offline/fast loads are the service worker
// cache's job; leaving these to the browser's heuristic HTTP
// caching let a stale app.js pair with a fresh index.html.
w.Header().Set("Cache-Control", "no-cache")
candidate := filepath.Join(*staticDir, filepath.FromSlash(r.URL.Path))
if r.URL.Path != "/" {
if info, err := os.Stat(candidate); err != nil || info.IsDir() {
+7 -1
View File
@@ -16,8 +16,14 @@ const ASSETS = [
];
self.addEventListener("install", (event) => {
// cache: "reload" bypasses the browser's HTTP cache, so a new build always
// caches assets fetched fresh from the server. Without it, addAll could mix
// a fresh index.html with a heuristically-cached stale app.js and install a
// build whose markup references listeners the old script never registers.
event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
caches.open(CACHE).then((cache) =>
cache.addAll(ASSETS.map((u) => new Request(u, { cache: "reload" })))
)
);
// No skipWaiting() here: a new worker stays in "waiting" while an old one is
// controlling a tab, so the page can prompt before swapping assets out from