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 8dc2a587717..1092f6e43db 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 e9f46d014c1..08a38e632eb 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 00000000000..5276c023786 --- /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 f76cd3c15a2..d889a1c6ac5 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 b2fee3bb9bb..912e90d20fc 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 4c46b2ca5f5..9d418e14af9 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 602921852d1..7ccbca5206f 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 9fbf4caf9a1..273a7a8769d 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 44798ac9912..119b0fba09c 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 4adb38360c6..83a31e93845 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 61de5bfbdcc..c10def67fda 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 837353e869b..a3ec66eef63 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 8c1088bff4f..8a509c6642d 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 32c05b72c1f..6a60e258933 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 fe6fe66cde3..7ebd844070d 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 fae839f79df..f92a5579bc7 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 7f988cc0481..c76d4a6c081 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 0c28184383a..21cfedcedd0 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 10b8fc156bd..6c37a83db38 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 440bbb7f8e1..7218205243f 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 01543ae8ede..4368b20a3fd 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 a7e7c047ae4..94c5d0ef986 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 50871c0865a..2dc85cda7fb 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",