Unverified Commit bdd3554b authored by memoclaw's avatar memoclaw Committed by GitHub

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

Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e5b9392f
import { AlertCircle, RefreshCw } from "lucide-react"; import { AlertCircle, RefreshCw } from "lucide-react";
import { Component, type ErrorInfo, type ReactNode } from "react"; import { Component, type ErrorInfo, type ReactNode } from "react";
import { useRouteError } from "react-router-dom";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
interface Props { interface Props {
...@@ -68,3 +69,35 @@ export class ErrorBoundary extends Component<Props, State> { ...@@ -68,3 +69,35 @@ export class ErrorBoundary extends Component<Props, State> {
return this.props.children; 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>
);
}
...@@ -24,7 +24,10 @@ const RootLayout = () => { ...@@ -24,7 +24,10 @@ const RootLayout = () => {
useEffect(() => { useEffect(() => {
if (!currentUser) { 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); navigateTo(ROUTES.EXPLORE);
} else { } else {
redirectOnAuthFailure(); redirectOnAuthFailure();
......
import { lazy } from "react"; import { lazy } from "react";
import { createBrowserRouter } from "react-router-dom"; import { createBrowserRouter } from "react-router-dom";
import App from "@/App"; import App from "@/App";
import { ChunkLoadErrorFallback } from "@/components/ErrorBoundary";
import MainLayout from "@/layouts/MainLayout"; import MainLayout from "@/layouts/MainLayout";
import RootLayout from "@/layouts/RootLayout"; import RootLayout from "@/layouts/RootLayout";
import Home from "@/pages/Home"; import Home from "@/pages/Home";
const AdminSignIn = lazy(() => import("@/pages/AdminSignIn")); // Wrap lazy imports to auto-reload on chunk load failure (e.g., after redeployment).
const Archived = lazy(() => import("@/pages/Archived")); function lazyWithReload<T extends React.ComponentType>(factory: () => Promise<{ default: T }>) {
const AuthCallback = lazy(() => import("@/pages/AuthCallback")); return lazy(() =>
const Explore = lazy(() => import("@/pages/Explore")); factory().catch((error) => {
const Inboxes = lazy(() => import("@/pages/Inboxes")); const isChunkError =
const MemoDetail = lazy(() => import("@/pages/MemoDetail")); error?.message?.includes("Failed to fetch dynamically imported module") ||
const NotFound = lazy(() => import("@/pages/NotFound")); error?.message?.includes("Importing a module script failed");
const PermissionDenied = lazy(() => import("@/pages/PermissionDenied")); const reloadKey = "chunk-reload";
const Attachments = lazy(() => import("@/pages/Attachments")); if (isChunkError && !sessionStorage.getItem(reloadKey)) {
const Setting = lazy(() => import("@/pages/Setting")); sessionStorage.setItem(reloadKey, "1");
const SignIn = lazy(() => import("@/pages/SignIn")); window.location.reload();
const SignUp = lazy(() => import("@/pages/SignUp")); }
const UserProfile = lazy(() => import("@/pages/UserProfile")); 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"; import { ROUTES } from "./routes";
...@@ -29,6 +47,7 @@ const router = createBrowserRouter([ ...@@ -29,6 +47,7 @@ const router = createBrowserRouter([
{ {
path: "/", path: "/",
element: <App />, element: <App />,
errorElement: <ChunkLoadErrorFallback />,
children: [ children: [
{ {
path: Routes.AUTH, path: Routes.AUTH,
......
...@@ -8,28 +8,23 @@ const PUBLIC_ROUTES = [ ...@@ -8,28 +8,23 @@ const PUBLIC_ROUTES = [
"/memos/", // Individual memo detail pages (dynamic) "/memos/", // Individual memo detail pages (dynamic)
] as const; ] as const;
const PRIVATE_ROUTES = [ROUTES.ROOT, ROUTES.ATTACHMENTS, ROUTES.INBOX, ROUTES.ARCHIVED, ROUTES.SETTING] as const;
function isPublicRoute(path: string): boolean { function isPublicRoute(path: string): boolean {
return PUBLIC_ROUTES.some((route) => path.startsWith(route)); return PUBLIC_ROUTES.some((route) => path.startsWith(route));
} }
function isPrivateRoute(path: string): boolean { export function redirectOnAuthFailure(forceRedirect = false): void {
return PRIVATE_ROUTES.includes(path as (typeof PRIVATE_ROUTES)[number]);
}
export function redirectOnAuthFailure(): void {
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
// Don't redirect if it's a public route // Already on auth page, nothing to do.
if (isPublicRoute(currentPath)) { if (currentPath.startsWith(ROUTES.AUTH)) {
return;
}
// Don't redirect if it's a public route (unless forced, e.g. public visibility is disallowed).
if (!forceRedirect && isPublicRoute(currentPath)) {
return; 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(); clearAccessToken();
window.location.replace(ROUTES.AUTH); window.location.replace(ROUTES.AUTH);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment