fix(start): 404 missing assets + immutable cache headers — stop post-deploy cache poisoning#351
fix(start): 404 missing assets + immutable cache headers — stop post-deploy cache poisoning#351ayushjhanwar-png wants to merge 1 commit into
Conversation
…deploy cache poisoning Missing /assets/* files fell through to the SSR router (307 to /login), so during every deploy Cloudflare and browsers cached HTML under .js chunk URLs for 4h, breaking module loading for all users until a manual cache purge. - assets-404.ts: unmatched /assets/** now returns 404 with no-store (nitro's static handler runs first, so real files are unaffected) - routeRules: hashed assets get max-age=31536000 immutable; everything else no-cache (origin previously sent no cache-control at all, Cloudflare defaulted to 4h) - router.tsx: reload once on vite:preloadError so tabs opened before a deploy recover from removed old chunks instead of white-screening - sdks: @openpanel/web workspace pin 1.1.0-local no longer matches 1.3.0-local, breaking root pnpm install; use workspace:*
📝 WalkthroughWalkthroughAdds client-side recovery for stale Vite preload chunk errors via page reload, a server-side 404 handler with no-store caching for missing asset requests, corresponding Nitro/Vite route caching rules, a new h3 devDependency, and workspace wildcard updates for ChangesDeploy Chunk Reload and Asset Caching
SDK Workspace Dependency Updates
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant ViteRouter as Router
participant Server as Nitro Server
participant CDN as Edge Cache
Browser->>ViteRouter: Import stale hashed chunk
ViteRouter->>Browser: vite:preloadError fired
ViteRouter->>Browser: sessionStorage check 'op:chunk-reload'
ViteRouter->>Browser: preventDefault() + reload()
Browser->>Server: Request /assets/*.js
Server->>Server: assets-404 handler checks file existence
alt asset missing
Server-->>Browser: 404, cache-control no-store
else asset exists
Server->>CDN: serve with immutable caching
CDN-->>Browser: cached asset response
end
Compact metadata
Poem 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/start/src/router.tsx`:
- Around line 8-23: The reload guard in the window-level vite:preloadError
listener can permanently block recovery for the same URL because the
sessionStorage marker is never cleared. Update the router.tsx logic so the guard
is reset after a successful load/navigation, using the existing key and the
current location href in the same listener setup. Keep the loop protection, but
clear or expire the stored marker once the page has loaded successfully so a
later chunk failure can still trigger window.location.reload again.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6d17acfe-09f0-4b74-9517-31e6f0bd8abe
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
apps/start/package.jsonapps/start/src/router.tsxapps/start/src/server/assets-404.tsapps/start/vite.config.tspackages/sdks/astro/package.jsonpackages/sdks/nextjs/package.json
| if (typeof window !== 'undefined') { | ||
| // After a deploy the previous build's hashed chunks no longer exist on the | ||
| // server, so tabs opened before the deploy fail dynamic imports on their | ||
| // next navigation. Reload once to pick up the new build; the sessionStorage | ||
| // guard prevents a reload loop if the chunk is still failing afterwards. | ||
| window.addEventListener('vite:preloadError', (event) => { | ||
| const key = 'op:chunk-reload'; | ||
| if (sessionStorage.getItem(key) === window.location.href) { | ||
| return; | ||
| } | ||
| sessionStorage.setItem(key, window.location.href); | ||
| event.preventDefault(); | ||
| window.location.reload(); | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Reload guard can permanently disable recovery for a given URL within a session.
The sessionStorage key is set once and never cleared, so if vite:preloadError fires again for the same URL later in the same tab session (e.g. a second deploy while the tab stays open), the guard silently skips the reload and the user stays stuck on stale chunks.
♻️ Suggested fix: clear the guard once the page has loaded successfully
window.addEventListener('vite:preloadError', (event) => {
const key = 'op:chunk-reload';
if (sessionStorage.getItem(key) === window.location.href) {
return;
}
sessionStorage.setItem(key, window.location.href);
event.preventDefault();
window.location.reload();
});
+ window.addEventListener('load', () => {
+ sessionStorage.removeItem('op:chunk-reload');
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (typeof window !== 'undefined') { | |
| // After a deploy the previous build's hashed chunks no longer exist on the | |
| // server, so tabs opened before the deploy fail dynamic imports on their | |
| // next navigation. Reload once to pick up the new build; the sessionStorage | |
| // guard prevents a reload loop if the chunk is still failing afterwards. | |
| window.addEventListener('vite:preloadError', (event) => { | |
| const key = 'op:chunk-reload'; | |
| if (sessionStorage.getItem(key) === window.location.href) { | |
| return; | |
| } | |
| sessionStorage.setItem(key, window.location.href); | |
| event.preventDefault(); | |
| window.location.reload(); | |
| }); | |
| } | |
| if (typeof window !== 'undefined') { | |
| // After a deploy the previous build's hashed chunks no longer exist on the | |
| // server, so tabs opened before the deploy fail dynamic imports on their | |
| // next navigation. Reload once to pick up the new build; the sessionStorage | |
| // guard prevents a reload loop if the chunk is still failing afterwards. | |
| window.addEventListener('vite:preloadError', (event) => { | |
| const key = 'op:chunk-reload'; | |
| if (sessionStorage.getItem(key) === window.location.href) { | |
| return; | |
| } | |
| sessionStorage.setItem(key, window.location.href); | |
| event.preventDefault(); | |
| window.location.reload(); | |
| }); | |
| window.addEventListener('load', () => { | |
| sessionStorage.removeItem('op:chunk-reload'); | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/start/src/router.tsx` around lines 8 - 23, The reload guard in the
window-level vite:preloadError listener can permanently block recovery for the
same URL because the sessionStorage marker is never cleared. Update the
router.tsx logic so the guard is reset after a successful load/navigation, using
the existing key and the current location href in the same listener setup. Keep
the loop protection, but clear or expire the stored marker once the page has
loaded successfully so a later chunk failure can still trigger
window.location.reload again.
Problem
After (almost) every dashboard deploy, users hit:
and the dashboard white-screens until a manual Cloudflare cache purge — and even then, affected browsers keep failing for up to 4 more hours from their local cache.
Root cause
/assets/*.jsfiles don't 404. Nitro only auto-404s missing assets for public-asset dirs mounted at a non-root baseURL; the TanStack plugin mounts the client build at/, so misses fall through to the SSR router →307 /login(unauthenticated) or a 200 HTML page (authenticated). Reproducible on prod:curl -I https://openpanel.dashverse.ai/assets/nonexistent.js→307 → /login.cache-controlon assets (nitropack 2.12's static handler never emits it), so Cloudflare applies its 4h default to.jsURLs.Fix
apps/start/src/server/assets-404.ts— route handler for/assets/**; Nitro's static middleware runs first, so this only fires for files that don't exist and returns404+cache-control: no-store. No cache layer can ever store a wrong answer for a chunk URL again.vite.config.tsrouteRules — hashed assets:public, max-age=31536000, immutable(content-addressed filenames, safe forever); everything else:no-cache.router.tsx— onvite:preloadError(chunk failed to load), reload once (sessionStorage guard against loops) so tabs opened before a deploy self-heal instead of white-screening.packages/sdks/{astro,nextjs}—@openpanel/webwas pinnedworkspace:1.1.0-localbut the package is now1.3.0-local, breaking rootpnpm installon main; switched toworkspace:*.Verified locally against the production build (
NITRO=1 pnpm build+node .output/server/index.mjs)/assets/x.jsno-store/assets/main-*.jsmax-age=31536000, immutableno-cachePost-merge ops (one-time)
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Chores