mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
reload on chunk errors, handle possible loops (#7854)
This commit is contained in:
parent
219cac8c4d
commit
0082d1fc2c
4 changed files with 73 additions and 1 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
31
packages/web/app/src/lib/chunk-error.ts
Normal file
31
packages/web/app/src/lib/chunk-error.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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} />);
|
||||
|
|
|
|||
|
|
@ -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/,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue