reload on chunk errors, handle possible loops (#7854)

This commit is contained in:
Jonathan Brennan 2026-03-17 03:29:56 -05:00 committed by GitHub
parent 219cac8c4d
commit 0082d1fc2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 1 deletions

View file

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { LogOutIcon } from 'lucide-react';
import { useSessionContext } from 'supertokens-auth-react/recipe/session';
import { Button } from '@/components/ui/button';
import { isChunkLoadError, reloadOnChunkError } from '@/lib/chunk-error';
import { captureException, flush } from '@sentry/react';
import { useRouter } from '@tanstack/react-router';
@ -16,9 +17,17 @@ export function ErrorComponent(props: { error: any; message?: string }) {
const session = useSessionContext();
useEffect(() => {
// If this is a stale chunk error, attempt a single reload to fetch fresh
// HTML. reloadOnChunkError() handles the sessionStorage guard internally
// to prevent infinite loops — see lib/chunk-error.ts for details.
// If it doesn't reload (flag already set), fall through to report to Sentry.
if (isChunkLoadError(props.error)) {
reloadOnChunkError();
}
captureException(props.error);
void flush(2000);
}, []);
}, [props.error]);
const isLoggedIn = !session.loading && session.doesSessionExist;

View file

@ -0,0 +1,31 @@
const CHUNK_RELOAD_KEY = 'chunk-reload';
/** Check if an error is a chunk/module load failure from a stale deployment. */
export function isChunkLoadError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
return (
// Chrome/Edge: failed dynamic import()
error.message.includes('Failed to fetch dynamically imported module') ||
// Safari/Firefox: failed dynamic import()
error.message.includes('Importing a module script failed') ||
// Webpack-style chunk errors (unlikely with Vite, but defensive)
error.name === 'ChunkLoadError'
);
}
// Reload the page once to recover from a stale chunk error. Uses sessionStorage
// to prevent infinite loops — if we've already reloaded once this session (flag
// not yet cleared by a successful boot), we let the error propagate instead.
export function reloadOnChunkError() {
if (sessionStorage.getItem(CHUNK_RELOAD_KEY)) return;
sessionStorage.setItem(CHUNK_RELOAD_KEY, '1');
window.location.reload();
}
// Clear the chunk-reload flag on successful app load. This ensures the
// auto-reload mechanism works again after a subsequent deployment. Without
// this, the flag from a previous reload would persist in sessionStorage
// and block future auto-reloads.
export function clearChunkReloadFlag() {
sessionStorage.removeItem(CHUNK_RELOAD_KEY);
}

View file

@ -2,6 +2,7 @@ import 'regenerator-runtime/runtime';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from '@tanstack/react-router';
import './index.css';
import { clearChunkReloadFlag, isChunkLoadError, reloadOnChunkError } from './lib/chunk-error';
import { router } from './router';
// Register things for typesafety
@ -13,4 +14,32 @@ declare module '@tanstack/react-router' {
Error.stackTraceLimit = 15;
clearChunkReloadFlag();
// After a deployment, JS chunk filenames change (content hashes). Users with a
// stale browser tab still reference old chunks that no longer exist on the CDN.
// Vite wraps dynamic imports with a preload helper that emits this event when
// a chunk or its CSS/JS dependencies fail to load. We catch it here and reload
// the page so the browser fetches fresh HTML with the correct chunk references.
// See: https://vite.dev/guide/build.html#load-error-handling
window.addEventListener('vite:preloadError', event => {
// Prevent the error from propagating — we're handling it with a reload.
event.preventDefault();
reloadOnChunkError();
});
// Not all dynamic imports go through Vite's preload wrapper. React.lazy() calls,
// nested dynamic imports inside already-loaded chunks, and third-party code (e.g.
// Monaco editor) do their own import() calls that Vite doesn't instrument. When
// these fail, the browser rejects the import promise with no handler, surfacing
// as an unhandled promise rejection. We check for the specific browser error
// messages that indicate a stale chunk, and reload if matched.
window.addEventListener('unhandledrejection', event => {
if (isChunkLoadError(event.reason)) {
// Suppress the rejection — a reload will resolve it.
event.preventDefault();
reloadOnChunkError();
}
});
ReactDOM.createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />);

View file

@ -107,6 +107,9 @@ if (env.sentry) {
"Failed to execute 'setStart' on 'Range'",
"Failed to execute 'setEnd' on 'Range'",
/TextModel got disposed/,
// Stale chunk errors after deployments — handled by auto-reload in main.tsx
/Failed to fetch dynamically imported module/,
/Importing a module script failed/,
],
});
}