1+ import { getErrorMessage , isUnauthenticatedError } from "@/lib/errors" ;
12import { sessionQueryKey } from "@/lib/queries/session" ;
2- import { queryClient } from "@/lib/query" ;
33import { Button } from "@repo/ui" ;
4- import { useQueryErrorResetBoundary } from "@tanstack/react-query" ;
4+ import {
5+ useQueryClient ,
6+ useQueryErrorResetBoundary ,
7+ } from "@tanstack/react-query" ;
58import { AlertCircle } from "lucide-react" ;
6- import * as React from "react" ;
79import { ErrorBoundary } from "react-error-boundary" ;
810
9- interface AuthErrorFallbackProps {
10- error : Error ;
11+ interface ResetProps {
1112 resetErrorBoundary : ( ) => void ;
1213}
1314
14- // Error type that may include status code
15- // NOTE: This extends Error to handle both tRPC errors (with status) and native JS errors
16- interface ErrorWithStatus extends Error {
17- status ?: number ;
18- }
19-
20- // Determine if an error is authentication-related
21- // WARNING: This function must catch all auth errors to prevent infinite error loops
22- // when wrapped components try to access protected resources
23- function isAuthError ( error : Error ) : boolean {
24- // Check for explicit status codes
25- const status = ( error as ErrorWithStatus ) ?. status ;
26- if ( status === 401 || status === 403 ) return true ;
15+ // Fallback for auth errors in protected routes
16+ function AuthErrorFallback ( { resetErrorBoundary } : ResetProps ) {
17+ const queryClient = useQueryClient ( ) ;
2718
28- // Check for auth-related error messages
29- const message = error . message ?. toLowerCase ( ) || "" ;
30- return (
31- message . includes ( "unauthorized" ) ||
32- message . includes ( "unauthenticated" ) ||
33- message . includes ( "session expired" ) ||
34- message . includes ( "401" ) ||
35- message . includes ( "403" )
36- ) ;
37- }
38-
39- // Fallback component for auth errors
40- function AuthErrorFallback ( {
41- error,
42- resetErrorBoundary,
43- } : AuthErrorFallbackProps ) {
4419 const handleRetry = ( ) => {
45- // Reset React Query errors and component errors
46- // NOTE: resetQueries() clears error state but preserves cached data,
47- // allowing immediate retry without full data refetch
48- queryClient . resetQueries ( ) ;
20+ queryClient . resetQueries ( { queryKey : sessionQueryKey } ) ;
4921 resetErrorBoundary ( ) ;
5022 } ;
5123
5224 const handleSignIn = ( ) => {
53- // Clear auth caches and redirect to login
54- // WARNING: removeQueries() permanently deletes cached auth data.
55- // Using window.location.href (not router navigation) ensures full page reload,
56- // clearing all React state that might hold stale auth tokens
57- queryClient . removeQueries ( { queryKey : [ "auth" ] } ) ;
58- window . location . href = "/login" ;
25+ queryClient . removeQueries ( { queryKey : sessionQueryKey } ) ;
26+ const { pathname, search, hash } = window . location ;
27+ const returnTo = encodeURIComponent ( pathname + search + hash ) ;
28+ window . location . href = `/login?returnTo=${ returnTo } ` ;
5929 } ;
6030
6131 return (
@@ -64,10 +34,7 @@ function AuthErrorFallback({
6434 < AlertCircle className = "mx-auto mb-4 h-12 w-12 text-destructive" />
6535 < h1 className = "mb-2 text-2xl font-bold" > Authentication Required</ h1 >
6636 < p className = "mb-6 text-muted-foreground" >
67- { error . message === "Unauthorized" ||
68- ( error as ErrorWithStatus ) ?. status === 401
69- ? "Your session has expired. Please sign in again."
70- : "You need to sign in to access this page." }
37+ Please sign in to access this page.
7138 </ p >
7239 < div className = "flex justify-center gap-3" >
7340 < Button variant = "outline" onClick = { handleRetry } >
@@ -80,53 +47,61 @@ function AuthErrorFallback({
8047 ) ;
8148}
8249
83- interface AuthErrorBoundaryProps {
84- children : React . ReactNode ;
50+ interface ErrorFallbackProps {
51+ error : unknown ;
52+ resetErrorBoundary : ( ) => void ;
8553}
8654
87- // General error fallback that handles both auth and other errors
88- // This wrapper ensures auth errors get special UI treatment while preserving
89- // generic error handling for all other failures
90- function ErrorFallbackWrapper ( {
55+ // Generic error fallback for non-auth errors
56+ function GenericErrorFallback ( {
9157 error,
9258 resetErrorBoundary,
93- } : AuthErrorFallbackProps ) {
94- // If it's not an auth error, show a generic error message
95- if ( ! isAuthError ( error ) ) {
96- return (
97- < div className = "flex min-h-svh flex-col items-center justify-center p-6" >
98- < div className = "mx-auto max-w-md text-center" >
99- < AlertCircle className = "mx-auto mb-4 h-12 w-12 text-destructive" />
100- < h1 className = "mb-2 text-2xl font-bold" > Something went wrong</ h1 >
101- < p className = "mb-6 text-muted-foreground" >
102- { error . message || "An unexpected error occurred" }
103- </ p >
104- < Button onClick = { resetErrorBoundary } > Try Again</ Button >
105- </ div >
59+ } : ErrorFallbackProps ) {
60+ return (
61+ < div className = "flex min-h-svh flex-col items-center justify-center p-6" >
62+ < div className = "mx-auto max-w-md text-center" >
63+ < AlertCircle className = "mx-auto mb-4 h-12 w-12 text-destructive" />
64+ < h1 className = "mb-2 text-2xl font-bold" > Something went wrong</ h1 >
65+ < p className = "mb-6 text-muted-foreground" > { getErrorMessage ( error ) } </ p >
66+ < Button onClick = { resetErrorBoundary } > Try Again</ Button >
10667 </ div >
107- ) ;
108- }
68+ </ div >
69+ ) ;
70+ }
10971
110- // For auth errors, use the auth-specific UI
111- return (
112- < AuthErrorFallback error = { error } resetErrorBoundary = { resetErrorBoundary } />
72+ interface ErrorBoundaryProps {
73+ children : React . ReactNode ;
74+ }
75+
76+ // Routes auth errors to AuthErrorFallback, others to GenericErrorFallback
77+ function AuthAwareErrorFallback ( {
78+ error,
79+ resetErrorBoundary,
80+ } : ErrorFallbackProps ) {
81+ return isUnauthenticatedError ( error ) ? (
82+ < AuthErrorFallback resetErrorBoundary = { resetErrorBoundary } />
83+ ) : (
84+ < GenericErrorFallback
85+ error = { error }
86+ resetErrorBoundary = { resetErrorBoundary }
87+ />
11388 ) ;
11489}
11590
116- // Modern auth error boundary using react-error-boundary
117- export function AuthErrorBoundary ( { children } : AuthErrorBoundaryProps ) {
91+ // Auth error boundary for protected routes only.
92+ // Catches auth errors (tRPC UNAUTHORIZED or HTTP 401) and shows recovery UI.
93+ // 403 (forbidden) falls through to generic handler since user IS authenticated.
94+ export function AuthErrorBoundary ( { children } : ErrorBoundaryProps ) {
95+ const queryClient = useQueryClient ( ) ;
11896 const { reset } = useQueryErrorResetBoundary ( ) ;
11997
12098 return (
12199 < ErrorBoundary
122- FallbackComponent = { ErrorFallbackWrapper }
100+ FallbackComponent = { AuthAwareErrorFallback }
123101 onReset = { reset }
124- onError = { ( error , errorInfo ) => {
125- console . error ( "Error caught by boundary:" , error , errorInfo ) ;
126- // Clear stale session data for auth errors
127- // NOTE: sessionQueryKey is imported from queries/session - ensures we clear
128- // the exact query key used by useSession() hook to prevent stale auth state
129- if ( isAuthError ( error ) ) {
102+ onError = { ( error ) => {
103+ console . error ( "Error caught by boundary:" , error ) ;
104+ if ( isUnauthenticatedError ( error ) ) {
130105 queryClient . removeQueries ( { queryKey : sessionQueryKey } ) ;
131106 }
132107 } }
@@ -135,3 +110,18 @@ export function AuthErrorBoundary({ children }: AuthErrorBoundaryProps) {
135110 </ ErrorBoundary >
136111 ) ;
137112}
113+
114+ // Generic error boundary for app root - no auth-specific handling
115+ export function AppErrorBoundary ( { children } : ErrorBoundaryProps ) {
116+ const { reset } = useQueryErrorResetBoundary ( ) ;
117+
118+ return (
119+ < ErrorBoundary
120+ FallbackComponent = { GenericErrorFallback }
121+ onReset = { reset }
122+ onError = { ( error ) => console . error ( "Uncaught error:" , error ) }
123+ >
124+ { children }
125+ </ ErrorBoundary >
126+ ) ;
127+ }
0 commit comments