From 62c79a500d3a7e8c0b091eb244a52bee31d0200a Mon Sep 17 00:00:00 2001 From: karilint Date: Fri, 29 May 2026 11:15:27 +0300 Subject: [PATCH 1/3] feat: Add ErrorBoundary and RouterError for better error UX Implement #1185: Provide ErrorBoundary and errorElement to catch and display application errors gracefully. - Add ErrorBoundary component to catch React component render errors - Add RouterError component to catch React Router v6 data router errors - Wrap RouterProvider with ErrorBoundary in main.tsx - Add errorElement to root route in router This provides a better user experience when the application encounters errors, showing a user-friendly error page with options to reload or go back instead of the default React error screen. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .../ErrorBoundary/ErrorBoundary.tsx | 108 ++++++++++++++++++ .../components/ErrorBoundary/RouterError.tsx | 80 +++++++++++++ frontend/src/main.tsx | 9 +- frontend/src/router/index.tsx | 2 + 4 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary/ErrorBoundary.tsx create mode 100644 frontend/src/components/ErrorBoundary/RouterError.tsx diff --git a/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 000000000..215a1403b --- /dev/null +++ b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,108 @@ +import { Component, ErrorInfo, ReactNode } from 'react' + +interface Props { + children: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + } + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + public componentDidCatch(_error: Error, _errorInfo: ErrorInfo): void { + // Error is logged to console by React in development + // In production, consider logging to an error tracking service + } + + public render(): ReactNode { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

The application encountered an unexpected error.

+ + {this.state.error && ( +
+ Error Details +
{this.state.error.toString()}
+

{this.state.error.stack}

+
+ )} +
+ ) + } + + return this.props.children + } +} + +const styles = { + container: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + padding: '2rem', + textAlign: 'center' as const, + backgroundColor: '#f5f5f5', + }, + heading: { + fontSize: '2rem', + color: '#d32f2f', + marginBottom: '1rem', + }, + message: { + fontSize: '1.2rem', + color: '#555', + marginBottom: '2rem', + }, + button: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + backgroundColor: '#1976d2', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + }, + details: { + marginTop: '2rem', + textAlign: 'left' as const, + maxWidth: '800px', + }, + summary: { + cursor: 'pointer', + color: '#1976d2', + fontWeight: 'bold' as const, + }, + pre: { + whiteSpace: 'pre-wrap' as const, + wordWrap: 'break-word' as const, + backgroundColor: '#fff', + padding: '1rem', + borderRadius: '4px', + border: '1px solid #ddd', + }, + stack: { + whiteSpace: 'pre-wrap' as const, + wordWrap: 'break-word' as const, + fontSize: '0.875rem', + color: '#757575', + marginTop: '1rem', + }, +} + +export default ErrorBoundary diff --git a/frontend/src/components/ErrorBoundary/RouterError.tsx b/frontend/src/components/ErrorBoundary/RouterError.tsx new file mode 100644 index 000000000..de96239be --- /dev/null +++ b/frontend/src/components/ErrorBoundary/RouterError.tsx @@ -0,0 +1,80 @@ +import { useRouteError } from 'react-router-dom' + +export const RouterError = () => { + const error = useRouteError() + + // Handle different error types that useRouteError can return + const errorMessage = + error instanceof Error + ? error.message + : error instanceof Response + ? `${error.status} ${error.statusText}` + : typeof error === 'string' + ? error + : 'An unknown error occurred' + + return ( +
+

Navigation Error

+

The page could not be loaded.

+

{errorMessage}

+ + +
+ ) +} + +const styles = { + container: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + padding: '2rem', + textAlign: 'center' as const, + backgroundColor: '#f5f5f5', + }, + heading: { + fontSize: '2rem', + color: '#d32f2f', + marginBottom: '1rem', + }, + message: { + fontSize: '1.2rem', + color: '#555', + marginBottom: '1rem', + }, + errorMessage: { + fontSize: '1rem', + color: '#757575', + marginBottom: '2rem', + fontFamily: 'monospace', + }, + button: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + backgroundColor: '#1976d2', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + margin: '0.5rem', + }, + secondaryButton: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + backgroundColor: 'transparent', + color: '#1976d2', + border: '1px solid #1976d2', + borderRadius: '4px', + cursor: 'pointer', + margin: '0.5rem', + }, +} + +export default RouterError diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index b0753c575..ef153658d 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,11 +5,14 @@ import { Provider } from 'react-redux' import './styles/global.css' import { RouterProvider } from 'react-router-dom' import router from './router' +import { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary' ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + ) diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 06b3f4cec..14b1f74d1 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,5 +1,6 @@ import { Navigate, createBrowserRouter } from 'react-router-dom' import App from '../App' +import { RouterError } from '../components/ErrorBoundary/RouterError' const loadPagesElement = async ( key: @@ -25,6 +26,7 @@ const router = createBrowserRouter([ { path: '/', element: , + errorElement: , children: [ { index: true, From 1d454e7d3e47236dc2df0872e3538ea7fb04b044 Mon Sep 17 00:00:00 2001 From: karilint Date: Fri, 29 May 2026 13:59:13 +0300 Subject: [PATCH 2/3] fix: Hide error details from production users in ErrorBoundary Security fix for PR #1186: ErrorBoundary was leaking internal implementation details (stack traces, error messages) to end users in production. - Gate error details section to development mode only using import.meta.env.DEV - In production, only show generic error message and reload button - Prevents information disclosure while maintaining debugging capability in dev Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- frontend/src/components/ErrorBoundary/ErrorBoundary.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx index 215a1403b..d76302cb3 100644 --- a/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -26,6 +26,9 @@ export class ErrorBoundary extends Component { public render(): ReactNode { if (this.state.hasError) { + // Only show error details in development mode to avoid leaking implementation details + const isDev = import.meta.env.DEV + return (

Something went wrong

@@ -33,7 +36,7 @@ export class ErrorBoundary extends Component { - {this.state.error && ( + {isDev && this.state.error && (
Error Details
{this.state.error.toString()}
From 88bed7adcac00c79006ab478cc547955b6ad608d Mon Sep 17 00:00:00 2001 From: karilint Date: Fri, 29 May 2026 15:22:58 +0300 Subject: [PATCH 3/3] fix: Hide router error details in production --- .../components/ErrorBoundary/RouterError.tsx | 14 ++++--------- .../ErrorBoundary/RouterErrorMessages.ts | 21 +++++++++++++++++++ .../src/tests/components/RouterError.test.ts | 15 +++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary/RouterErrorMessages.ts create mode 100644 frontend/src/tests/components/RouterError.test.ts diff --git a/frontend/src/components/ErrorBoundary/RouterError.tsx b/frontend/src/components/ErrorBoundary/RouterError.tsx index de96239be..ac14278d9 100644 --- a/frontend/src/components/ErrorBoundary/RouterError.tsx +++ b/frontend/src/components/ErrorBoundary/RouterError.tsx @@ -1,17 +1,11 @@ import { useRouteError } from 'react-router-dom' +import { ENV } from '@/util/config' +import { getRouterErrorMessage } from './RouterErrorMessages' + export const RouterError = () => { const error = useRouteError() - - // Handle different error types that useRouteError can return - const errorMessage = - error instanceof Error - ? error.message - : error instanceof Response - ? `${error.status} ${error.statusText}` - : typeof error === 'string' - ? error - : 'An unknown error occurred' + const errorMessage = getRouterErrorMessage(error, ENV === 'dev') return (
diff --git a/frontend/src/components/ErrorBoundary/RouterErrorMessages.ts b/frontend/src/components/ErrorBoundary/RouterErrorMessages.ts new file mode 100644 index 000000000..90e00bce5 --- /dev/null +++ b/frontend/src/components/ErrorBoundary/RouterErrorMessages.ts @@ -0,0 +1,21 @@ +const genericErrorMessage = 'An unexpected navigation error occurred.' + +export const getRouterErrorMessage = (error: unknown, isDevelopment: boolean): string => { + if (!isDevelopment) { + return genericErrorMessage + } + + if (error instanceof Error) { + return error.message + } + + if (error instanceof Response) { + return `${error.status} ${error.statusText}` + } + + if (typeof error === 'string') { + return error + } + + return 'An unknown error occurred' +} diff --git a/frontend/src/tests/components/RouterError.test.ts b/frontend/src/tests/components/RouterError.test.ts new file mode 100644 index 000000000..9288fe8d3 --- /dev/null +++ b/frontend/src/tests/components/RouterError.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from '@jest/globals' + +import { getRouterErrorMessage } from '@/components/ErrorBoundary/RouterErrorMessages' + +describe('getRouterErrorMessage', () => { + it('returns a generic message outside development', () => { + expect(getRouterErrorMessage(new Error('sensitive route loader details'), false)).toBe( + 'An unexpected navigation error occurred.' + ) + }) + + it('returns raw error details in development', () => { + expect(getRouterErrorMessage(new Error('route loader failed'), true)).toBe('route loader failed') + }) +})