From 1c8a60f858d8472aa56e29994ec1c7926da2c6ce Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Mon, 10 Aug 2026 17:34:02 +0530 Subject: [PATCH 1/2] [WEB-8632] fix(web): auto-reload on stale chunk load failure during navigation (#9579) * fix(web): implement stale asset recovery in entry client and error boundary Added event listeners in entry.client.tsx to handle stale asset errors in production, allowing for recovery from stale assets. Updated the ErrorBoundary component in root.tsx to utilize the same recovery mechanism for stale chunk failures, enhancing error handling and user experience. * fix(web): improve stale asset recovery logic in entry client Refactored the recoverFromStaleAsset function to return a boolean indicating whether a reload was triggered, enhancing the handling of stale asset errors. Updated the event listener in entry.client.tsx to prevent default behavior only when a recovery is in progress, improving error management in production environments. --- apps/web/app/entry.client.tsx | 18 +++++++ apps/web/app/root.tsx | 7 +++ apps/web/core/lib/stale-asset-error.ts | 66 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 apps/web/core/lib/stale-asset-error.ts diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx index 8dc2a58771..1092f6e43d 100644 --- a/apps/web/app/entry.client.tsx +++ b/apps/web/app/entry.client.tsx @@ -9,9 +9,27 @@ import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; import polyfills from "@/lib/polyfills"; +import { isStaleAssetErrorMessage, recoverFromStaleAsset } from "@/lib/stale-asset-error"; void polyfills; +// Production-only: in dev these errors come from the dev server itself (restarts, +// stale optimized deps) and auto-reloading would mask them. +if (import.meta.env.PROD) { + window.addEventListener("vite:preloadError", (event) => { + if (recoverFromStaleAsset()) event.preventDefault(); + }); + + window.addEventListener("error", (event) => { + if (isStaleAssetErrorMessage(event.message || "")) recoverFromStaleAsset(); + }); + + window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason instanceof Error ? event.reason.message : String(event.reason ?? ""); + if (isStaleAssetErrorMessage(reason)) recoverFromStaleAsset(); + }); +} + startTransition(() => { hydrateRoot( document, diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx index e9f46d014c..08a38e632e 100644 --- a/apps/web/app/root.tsx +++ b/apps/web/app/root.tsx @@ -24,6 +24,8 @@ import globalStyles from "@/styles/globals.css?url"; import type { Route } from "./+types/root"; // components import { LogoSpinner } from "@/components/common/logo-spinner"; +// lib +import { isStaleAssetError, recoverFromStaleAsset } from "@/lib/stale-asset-error"; // local import { CustomErrorComponent } from "./error"; import { AppProvider } from "./provider"; @@ -146,5 +148,10 @@ export function HydrateFallback() { } export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + // A stale chunk failure surfaces here as React Router's own wrapper error + // (the failed dynamic import itself never reaches a window event) — recover + // the same way entry.client.tsx does instead of just showing the error page. + if (import.meta.env.PROD && isStaleAssetError(error)) recoverFromStaleAsset(); + return ; } diff --git a/apps/web/core/lib/stale-asset-error.ts b/apps/web/core/lib/stale-asset-error.ts new file mode 100644 index 0000000000..5276c02378 --- /dev/null +++ b/apps/web/core/lib/stale-asset-error.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +// Error signatures produced when a deploy removes hashed assets that an open tab +// still references (stale-tab version skew). +const STALE_ASSET_ERROR_SIGNATURES = [ + "Failed to fetch dynamically imported module", // Chromium + "error loading dynamically imported module", // Firefox + "Importing a module script failed", // Safari + "Unable to preload CSS", // Vite preload helper + // React Router's own wrapper when a route module's lazy import fails during + // navigation — the original browser error above never reaches here as a + // window event, only this synthesized message. + "No result returned from dataStrategy for route", +]; + +export const isStaleAssetErrorMessage = (message: string): boolean => + STALE_ASSET_ERROR_SIGNATURES.some((signature) => message.includes(signature)); + +export const isStaleAssetError = (error: unknown): boolean => { + const message = error instanceof Error ? error.message : typeof error === "string" ? error : ""; + return isStaleAssetErrorMessage(message); +}; + +// Reload-once-then-fall-through-to-boundary guard, shared by the window-level +// listeners (entry.client.tsx) and the route ErrorBoundary (root.tsx) so both +// paths to the same failure share one reload attempt instead of racing. +const STALE_ASSET_RELOAD_KEY = "__plane_chunk_reload"; +const STALE_ASSET_RELOAD_WINDOW_MS = 30_000; + +const hasRecentStaleAssetReload = (): boolean => { + try { + const lastReloadAt = Number(sessionStorage.getItem(STALE_ASSET_RELOAD_KEY)); + return Number.isFinite(lastReloadAt) && Date.now() - lastReloadAt < STALE_ASSET_RELOAD_WINDOW_MS; + } catch { + // No storage means no loop guard — never auto-reload in that case. + return true; + } +}; + +let isRecoveringFromStaleAsset = false; + +// Returns whether it actually triggered a reload, so callers that can otherwise +// fall back to default browser error handling (e.g. Vite's preload-error +// overlay) only suppress that fallback when a reload is really in flight. +export const recoverFromStaleAsset = (): boolean => { + if (isRecoveringFromStaleAsset) return false; + isRecoveringFromStaleAsset = true; + + if (hasRecentStaleAssetReload()) { + // Second failure in a row — surface to the route error boundary instead of looping. + isRecoveringFromStaleAsset = false; + return false; + } + try { + sessionStorage.setItem(STALE_ASSET_RELOAD_KEY, String(Date.now())); + } catch { + isRecoveringFromStaleAsset = false; + return false; + } + window.location.reload(); + return true; +}; From e056bbf9eb6b511cdc0a5823b1bd6922e561a485 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 16 Aug 2026 23:36:30 +0530 Subject: [PATCH 2/2] chore: dump version --- apps/admin/package.json | 2 +- apps/api/package.json | 2 +- apps/live/package.json | 2 +- apps/space/package.json | 2 +- apps/web/package.json | 2 +- package.json | 2 +- packages/codemods/package.json | 2 +- packages/constants/package.json | 2 +- packages/editor/package.json | 2 +- packages/hooks/package.json | 2 +- packages/i18n/package.json | 2 +- packages/logger/package.json | 2 +- packages/propel/package.json | 2 +- packages/services/package.json | 2 +- packages/shared-state/package.json | 2 +- packages/tailwind-config/package.json | 2 +- packages/types/package.json | 2 +- packages/typescript-config/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/admin/package.json b/apps/admin/package.json index f76cd3c15a..d889a1c6ac 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -1,6 +1,6 @@ { "name": "admin", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "Admin UI for Plane", "license": "AGPL-3.0", diff --git a/apps/api/package.json b/apps/api/package.json index b2fee3bb9b..912e90d20f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "plane-api", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "API server powering Plane's backend", "license": "AGPL-3.0" diff --git a/apps/live/package.json b/apps/live/package.json index 4c46b2ca5f..9d418e14af 100644 --- a/apps/live/package.json +++ b/apps/live/package.json @@ -1,6 +1,6 @@ { "name": "live", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "A realtime collaborative server powers Plane's rich text editor", "license": "AGPL-3.0", diff --git a/apps/space/package.json b/apps/space/package.json index 602921852d..7ccbca5206 100644 --- a/apps/space/package.json +++ b/apps/space/package.json @@ -1,6 +1,6 @@ { "name": "space", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index 9fbf4caf9a..273a7a8769 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/package.json b/package.json index 44798ac991..119b0fba09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plane", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "Open-source project management that unlocks customer value", "license": "AGPL-3.0", diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 4adb38360c..83a31e9384 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,6 +1,6 @@ { "name": "@plane/codemods", - "version": "1.4.1", + "version": "1.4.2", "private": true, "scripts": { "test": "vitest run", diff --git a/packages/constants/package.json b/packages/constants/package.json index 61de5bfbdc..c10def67fd 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -1,6 +1,6 @@ { "name": "@plane/constants", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/editor/package.json b/packages/editor/package.json index 837353e869..a3ec66eef6 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@plane/editor", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "Core Editor that powers Plane", "keywords": [ diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 8c1088bff4..8a509c6642 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@plane/hooks", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "React hooks that are shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 32c05b72c1..6a60e25893 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@plane/i18n", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "I18n shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/logger/package.json b/packages/logger/package.json index fe6fe66cde..7ebd844070 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@plane/logger", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "Logger shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/propel/package.json b/packages/propel/package.json index fae839f79d..f92a5579bc 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -1,6 +1,6 @@ { "name": "@plane/propel", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/services/package.json b/packages/services/package.json index 7f988cc048..c76d4a6c08 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -1,6 +1,6 @@ { "name": "@plane/services", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json index 0c28184383..21cfedcedd 100644 --- a/packages/shared-state/package.json +++ b/packages/shared-state/package.json @@ -1,6 +1,6 @@ { "name": "@plane/shared-state", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "Shared state shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json index 10b8fc156b..6c37a83db3 100644 --- a/packages/tailwind-config/package.json +++ b/packages/tailwind-config/package.json @@ -1,6 +1,6 @@ { "name": "@plane/tailwind-config", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "common tailwind configuration across monorepo", "license": "AGPL-3.0", diff --git a/packages/types/package.json b/packages/types/package.json index 440bbb7f8e..7218205243 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@plane/types", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "type": "module", diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 01543ae8ed..4368b20a3f 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@plane/typescript-config", - "version": "1.4.1", + "version": "1.4.2", "private": true, "license": "AGPL-3.0", "files": [ diff --git a/packages/ui/package.json b/packages/ui/package.json index a7e7c047ae..94c5d0ef98 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@plane/ui", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "UI components shared across multiple apps internally", "license": "AGPL-3.0", diff --git a/packages/utils/package.json b/packages/utils/package.json index 50871c0865..2dc85cda7f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@plane/utils", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "Helper functions shared across multiple apps internally", "license": "AGPL-3.0",