fix: handle chunk load errors after redeployment with auto-reload (#5703)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
memoclaw 2026-03-08 10:45:16 +08:00 committed by GitHub
parent e5b9392fcd
commit bdd3554b89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 28 deletions

View File

@ -1,5 +1,6 @@
import { AlertCircle, RefreshCw } from "lucide-react";
import { Component, type ErrorInfo, type ReactNode } from "react";
import { useRouteError } from "react-router-dom";
import { Button } from "./ui/button";
interface Props {
@ -68,3 +69,35 @@ export class ErrorBoundary extends Component<Props, State> {
return this.props.children;
}
}
// React Router errorElement for route-level errors (e.g., failed chunk loads after redeployment).
export function ChunkLoadErrorFallback() {
const error = useRouteError() as Error | undefined;
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="max-w-md w-full p-6 space-y-4">
<div className="flex items-center gap-3 text-destructive">
<AlertCircle className="w-8 h-8" />
<h1 className="text-2xl font-bold">Something went wrong</h1>
</div>
<p className="text-foreground/70">
An unexpected error occurred. This could be due to a network issue or an application update. Reloading usually fixes it.
</p>
{error?.message && (
<details className="bg-muted p-3 rounded-md text-sm">
<summary className="cursor-pointer font-medium mb-2">Error details</summary>
<pre className="whitespace-pre-wrap break-words text-xs text-foreground/60">{error.message}</pre>
</details>
)}
<Button onClick={() => window.location.reload()} className="w-full gap-2">
<RefreshCw className="w-4 h-4" />
Reload Application
</Button>
</div>
</div>
);
}

View File

@ -24,7 +24,10 @@ const RootLayout = () => {
useEffect(() => {
if (!currentUser) {
if (pathname === ROUTES.ROOT && !memoRelatedSetting.disallowPublicVisibility) {
if (memoRelatedSetting.disallowPublicVisibility) {
// When public visibility is disallowed, always redirect unauth users to auth.
redirectOnAuthFailure(true);
} else if (pathname === ROUTES.ROOT) {
navigateTo(ROUTES.EXPLORE);
} else {
redirectOnAuthFailure();

View File

@ -1,23 +1,41 @@
import { lazy } from "react";
import { createBrowserRouter } from "react-router-dom";
import App from "@/App";
import { ChunkLoadErrorFallback } from "@/components/ErrorBoundary";
import MainLayout from "@/layouts/MainLayout";
import RootLayout from "@/layouts/RootLayout";
import Home from "@/pages/Home";
const AdminSignIn = lazy(() => import("@/pages/AdminSignIn"));
const Archived = lazy(() => import("@/pages/Archived"));
const AuthCallback = lazy(() => import("@/pages/AuthCallback"));
const Explore = lazy(() => import("@/pages/Explore"));
const Inboxes = lazy(() => import("@/pages/Inboxes"));
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
const NotFound = lazy(() => import("@/pages/NotFound"));
const PermissionDenied = lazy(() => import("@/pages/PermissionDenied"));
const Attachments = lazy(() => import("@/pages/Attachments"));
const Setting = lazy(() => import("@/pages/Setting"));
const SignIn = lazy(() => import("@/pages/SignIn"));
const SignUp = lazy(() => import("@/pages/SignUp"));
const UserProfile = lazy(() => import("@/pages/UserProfile"));
// Wrap lazy imports to auto-reload on chunk load failure (e.g., after redeployment).
function lazyWithReload<T extends React.ComponentType>(factory: () => Promise<{ default: T }>) {
return lazy(() =>
factory().catch((error) => {
const isChunkError =
error?.message?.includes("Failed to fetch dynamically imported module") ||
error?.message?.includes("Importing a module script failed");
const reloadKey = "chunk-reload";
if (isChunkError && !sessionStorage.getItem(reloadKey)) {
sessionStorage.setItem(reloadKey, "1");
window.location.reload();
}
throw error;
}),
);
}
const AdminSignIn = lazyWithReload(() => import("@/pages/AdminSignIn"));
const Archived = lazyWithReload(() => import("@/pages/Archived"));
const AuthCallback = lazyWithReload(() => import("@/pages/AuthCallback"));
const Explore = lazyWithReload(() => import("@/pages/Explore"));
const Inboxes = lazyWithReload(() => import("@/pages/Inboxes"));
const MemoDetail = lazyWithReload(() => import("@/pages/MemoDetail"));
const NotFound = lazyWithReload(() => import("@/pages/NotFound"));
const PermissionDenied = lazyWithReload(() => import("@/pages/PermissionDenied"));
const Attachments = lazyWithReload(() => import("@/pages/Attachments"));
const Setting = lazyWithReload(() => import("@/pages/Setting"));
const SignIn = lazyWithReload(() => import("@/pages/SignIn"));
const SignUp = lazyWithReload(() => import("@/pages/SignUp"));
const UserProfile = lazyWithReload(() => import("@/pages/UserProfile"));
import { ROUTES } from "./routes";
@ -29,6 +47,7 @@ const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ChunkLoadErrorFallback />,
children: [
{
path: Routes.AUTH,

View File

@ -8,28 +8,23 @@ const PUBLIC_ROUTES = [
"/memos/", // Individual memo detail pages (dynamic)
] as const;
const PRIVATE_ROUTES = [ROUTES.ROOT, ROUTES.ATTACHMENTS, ROUTES.INBOX, ROUTES.ARCHIVED, ROUTES.SETTING] as const;
function isPublicRoute(path: string): boolean {
return PUBLIC_ROUTES.some((route) => path.startsWith(route));
}
function isPrivateRoute(path: string): boolean {
return PRIVATE_ROUTES.includes(path as (typeof PRIVATE_ROUTES)[number]);
}
export function redirectOnAuthFailure(): void {
export function redirectOnAuthFailure(forceRedirect = false): void {
const currentPath = window.location.pathname;
// Don't redirect if it's a public route
if (isPublicRoute(currentPath)) {
// Already on auth page, nothing to do.
if (currentPath.startsWith(ROUTES.AUTH)) {
return;
}
// Always redirect to auth page on auth failure - the user's session expired
// and they should re-authenticate rather than being sent to explore.
if (isPrivateRoute(currentPath)) {
clearAccessToken();
window.location.replace(ROUTES.AUTH);
// Don't redirect if it's a public route (unless forced, e.g. public visibility is disallowed).
if (!forceRedirect && isPublicRoute(currentPath)) {
return;
}
clearAccessToken();
window.location.replace(ROUTES.AUTH);
}