A page from before a deploy can still find its chunks, or reload once

Sentry WIZWAR-5: a table open through a deploy asked for the replay
module by its old hashed name, the deploy had deleted it, and the
server answered with the app shell as text/html — "not a valid
JavaScript MIME type". Three guards: deploys keep old chunks for a
week instead of deleting them; a missing asset is a plain 404, never
the shell; and the client reloads once when Vite reports a failed
import, so the live bundle takes over.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-04 20:30:39 -04:00
co-authored by Claude Fable 5.1
parent fa5a5a07a7
commit 0911312f98
3 changed files with 25 additions and 2 deletions
+8 -1
View File
@@ -387,7 +387,14 @@ const httpServer = createServer((req, res) => {
res.writeHead(403).end();
return;
}
if (!existsSync(file)) file = join(staticRoot, "index.html"); // SPA fallback
if (!existsSync(file)) {
// A hashed asset that is gone is gone: a page built before the last
// deploy asking for its old chunk gets a plain 404 it can act on,
// never the app shell as text/html. Every other unknown path is the
// app's to route.
if (url.startsWith("/assets/")) { res.writeHead(404, { "content-type": "text/plain" }).end("gone"); return; }
file = join(staticRoot, "index.html");
}
// Resolve symlinks and re-verify the real location stays inside the root.
const real = realpathSync(file);
if (real !== staticRoot && !real.startsWith(staticRoot + sep)) {
+13
View File
@@ -7,6 +7,19 @@ if (import.meta.env.VITE_SENTRY_DSN) {
Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, environment: "production" });
}
// A page loaded before a deploy may ask for a chunk the deploy retired.
// Vite reports the failed import here; one reload fetches the live bundle.
// The flag keeps a broken build from reloading forever.
window.addEventListener("vite:preloadError", (e) => {
e.preventDefault();
try {
if (sessionStorage.getItem("wizwar-reloaded")) return;
sessionStorage.setItem("wizwar-reloaded", "1");
} catch { /* storage may be unavailable; reload anyway */ }
location.reload();
});
try { sessionStorage.removeItem("wizwar-reloaded"); } catch { /* ignore */ }
// /watch/<id> is the public share page — one turn, anyone may look. It
// loads its own component so the full app (and its live socket appetite)
// stays out of a shared link's way.