diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 36de3b95706..e9106640c70 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -7,6 +7,7 @@ import { type ErrorComponentProps, useLocation, useNavigate, + useRouter, } from "@tanstack/react-router"; import { useEffect, useEffectEvent, useRef, useState } from "react"; @@ -188,6 +189,36 @@ function RootRouteErrorView({ error, reset }: ErrorComponentProps) { const message = errorMessage(error); const details = errorDetails(error); + // The root error boundary is shown when beforeLoad's startup probe + // (resolveInitialServerAuthGateState -> fetchSessionState) throws. That failure + // is frequently transient (backend still starting, a flaky session call), but + // the boundary persists until a manual reload. Auto-retry when the window + // regains focus/visibility or the network returns, so a recovered backend + // un-sticks the app instead of stranding the user here. reset() only resets the + // boundary; router.invalidate() re-runs beforeLoad. See #3513. + const router = useRouter(); + const retryingRef = useRef(false); + useEffect(() => { + const retry = () => { + if (retryingRef.current) return; + retryingRef.current = true; + void router.invalidate().finally(() => { + retryingRef.current = false; + }); + }; + const retryIfVisible = () => { + if (document.visibilityState === "visible") retry(); + }; + document.addEventListener("visibilitychange", retryIfVisible); + window.addEventListener("focus", retry); + window.addEventListener("online", retry); + return () => { + document.removeEventListener("visibilitychange", retryIfVisible); + window.removeEventListener("focus", retry); + window.removeEventListener("online", retry); + }; + }, [router]); + return (