Compare commits

...

2 Commits

Author SHA1 Message Date
lebaudantoine bd51c142f6 🐛(frontend) return real 404s for missing hashed assets
Nginx was rewriting missing `/assets/*` files to `index.html`, so
requests for stale chunks got a 200 with the SPA shell served as a
JS module. That is what turned a plain 404 into the confusing:

  "TypeError: error loading dynamically imported module"

whenever a client loaded before a deployment requested an old chunk.

Stop rewriting missing `/assets/*` to `index.html`: those requests
now return a real 404 with `Cache-Control: no-store`. SPA route
fallback for non-asset paths is unchanged.
2026-08-12 19:40:47 +02:00
lebaudantoine 2af6157265 (frontend) recover from stale lazy-loaded chunks after a deploy
Route components are code-split with content-hashed filenames. A
user who loaded the app before a deployment (typically someone
sitting in a call) still holds an `index.html` referencing old chunk
names. When they navigate after the deploy — e.g. to `/feedback` on
leaving a room — the old chunk is gone and the dynamic import fails
with "TypeError: error loading dynamically imported module".

Reload the page on that failure to fetch a fresh `index.html` with
the current hashes, which transparently fixes the stale-deploy case.

Guard against infinite reload loops with a `RELOAD_COOLDOWN_MS`: if
the import fails again right after a reload, the cause is not a
stale deploy (ad blocker, proxy, outage) and reloading further would
loop forever. In that case, let the error propagate so it reaches
monitoring.

Inspired by https://vite.dev/guide/build#load-error-handling
2026-08-12 19:37:24 +02:00
4 changed files with 38 additions and 0 deletions
+4
View File
@@ -8,6 +8,10 @@ and this project adheres to
## [Unreleased]
### Fixed
- ✨(frontend) recover from stale lazy-loaded chunks after a deploy
## [1.26.0] - 2026-08-12
### Added
+7
View File
@@ -74,6 +74,13 @@ server {
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
error_page 404 = @asset_missing;
}
location @asset_missing {
add_header Cache-Control "no-store" always;
return 404;
}
# Serve static files
+7
View File
@@ -14,6 +14,13 @@ server {
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
error_page 404 = @asset_missing;
}
location @asset_missing {
add_header Cache-Control "no-store" always;
return 404;
}
# Serve static files
+20
View File
@@ -2,6 +2,26 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
const CHUNK_RELOAD_KEY = 'vite-preload-error-reload-at'
const RELOAD_COOLDOWN_MS = 30_000
window.addEventListener('vite:preloadError', (event) => {
try {
const lastReloadAt = Number(sessionStorage.getItem(CHUNK_RELOAD_KEY)) || 0
if (Date.now() - lastReloadAt <= RELOAD_COOLDOWN_MS) {
// Recent reload didn't help: not a stale deploy, surface the error.
return
}
sessionStorage.setItem(CHUNK_RELOAD_KEY, String(Date.now()))
} catch {
// Without sessionStorage we cannot guard against a reload loop:
// don't auto-reload, let the error propagate.
return
}
event.preventDefault()
window.location.reload()
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />