Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Catching trpc error early before react router error boundary #8757

Merged
merged 12 commits into from
Feb 27, 2025
71 changes: 71 additions & 0 deletions packages/client/src/v2-events/routes/TRPCErrorBoundary.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/

import React from 'react'
import { Meta, StoryFn } from '@storybook/react'
import { TRPCClientError } from '@trpc/client'
import { TRPCProvider } from '@client/v2-events/trpc'
import { TRPCErrorBoundary } from '@client/v2-events/routes/TRPCErrorBoundary'

const meta: Meta<typeof TRPCErrorBoundary> = {
title: 'Components/TRPCErrorBoundary',
decorators: [
(Story) => (
<TRPCErrorBoundary>
<TRPCProvider>
<Story />
</TRPCProvider>
</TRPCErrorBoundary>
)
]
}

export default meta

// Default story: No error, renders children normally
export const Default: StoryFn<typeof TRPCErrorBoundary> = () => (
<TRPCErrorBoundary>
<div>
<h2>{'Normal Render'}</h2>
<p>
{'This content is inside the error boundary and renders correctly.'}
</p>
</div>
</TRPCErrorBoundary>
)

// Story that simulates an error
export const WithError: StoryFn<typeof TRPCErrorBoundary> = () => (
<TRPCErrorBoundary>
{(() => {
throw new Error('This is a test error for TRPCErrorBoundary.')
})()}
</TRPCErrorBoundary>
)

// Story for 401 Unauthorized Error
export const UnauthorizedError: StoryFn<typeof TRPCErrorBoundary> = () => (
<TRPCErrorBoundary>
{(() => {
;(() => {
const error = new TRPCClientError<never>('Unauthorized', {
meta: {
response: {
status: 401,
statusText: 'Unauthorized'
}
}
})
throw error
})()
})()}
</TRPCErrorBoundary>
)
150 changes: 150 additions & 0 deletions packages/client/src/v2-events/routes/TRPCErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* eslint-disable react/destructuring-assignment */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import React, { Component } from 'react'
import * as Sentry from '@sentry/react'
import styled from 'styled-components'
import { TRPCClientError } from '@trpc/client'
import { connect } from 'react-redux'
import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl'
import { PageWrapper } from '@opencrvs/components/lib/PageWrapper'
import { TertiaryButton } from '@opencrvs/components/lib/buttons'
import { Box } from '@opencrvs/components/lib/Box'
import { errorMessages, buttonMessages } from '@client/i18n/messages'
// eslint-disable-next-line no-restricted-imports
import { redirectToAuthentication } from '@client/profile/profileActions'

const ErrorContainer = styled(Box)`
display: flex;
width: 400px;
flex-direction: column;
align-items: center;
margin-top: -80px;
`
const ErrorTitle = styled.h1`
${({ theme }) => theme.fonts.h1};
color: ${({ theme }) => theme.colors.copy};
margin-bottom: 16px;
`

const ErrorMessage = styled.div`
${({ theme }) => theme.fonts.reg18};
color: ${({ theme }) => theme.colors.copy};
margin-bottom: 32px;
text-align: center;
`

const development = ['127.0.0.1', 'localhost'].includes(
window.location.hostname
)

interface Props extends IntlShapeProps {
children: React.ReactNode
redirectToAuthentication: typeof redirectToAuthentication
}

interface State {
error: Error | null
}

class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { error: null }
}

static getDerivedStateFromError(error: Error) {
return { error }
}

componentDidCatch(error: Error) {
// eslint-disable-next-line no-console
console.error('TRPC Error Caught:', error)
}

render() {
// eslint-disable-next-line no-shadow
const { intl, redirectToAuthentication } = this.props
if (this.state.error) {
const error = this.state.error
let httpCode = 500
let message = error.message

if (error instanceof TRPCClientError) {
if (
error.meta &&
typeof error.meta === 'object' &&
'response' in error.meta &&
error.meta.response &&
typeof error.meta.response === 'object' &&
'status' in error.meta.response &&
'statusText' in error.meta.response
) {
httpCode = Number(error.meta.response.status)
message = String(error.meta.response.statusText)
}
}
/**
* TODO: Improve the error message design once the probable errors are defined
* and the design/ux is ready.
*/
return (
<Sentry.ErrorBoundary
showDialog={!development}
onError={(err) => {
// eslint-disable-next-line no-console
console.log('Sentry.ErrorBoundary: ', err)
}}
>
<PageWrapper>
<ErrorContainer>
{httpCode === 401 ? (
<>
<ErrorTitle>
{intl.formatMessage(errorMessages.errorTitleUnauthorized)}
</ErrorTitle>
<ErrorMessage>
{intl.formatMessage(errorMessages.errorCodeUnauthorized)}
</ErrorMessage>
<TertiaryButton
id="GoToLoginPage"
onClick={() => redirectToAuthentication(true)}
>
{intl.formatMessage(buttonMessages.login)}
</TertiaryButton>
</>
) : (
<>
<ErrorTitle>
{intl.formatMessage(errorMessages.errorTitle)}
</ErrorTitle>
<ErrorMessage>{message}</ErrorMessage>
<TertiaryButton
id="GoToHomepage"
onClick={() => (window.location.href = '/')}
>
{intl.formatMessage(buttonMessages.goToHomepage)}
</TertiaryButton>
</>
)}
</ErrorContainer>
</PageWrapper>
</Sentry.ErrorBoundary>
)
}

return this.props.children
}
}

export const TRPCErrorBoundary = connect(null, { redirectToAuthentication })(
injectIntl(ErrorBoundary)
)
12 changes: 8 additions & 4 deletions packages/client/src/v2-events/routes/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import React from 'react'
import { Outlet, RouteObject } from 'react-router-dom'

import { Debug } from '@client/v2-events/features/debug/debug'
import { router as correctionRouter } from '@client/v2-events/features/events/actions/correct/request/router'
import * as Declare from '@client/v2-events/features/events/actions/declare'
Expand All @@ -24,6 +25,7 @@ import { router as workqueueRouter } from '@client/v2-events/features/workqueues
import { EventOverviewLayout } from '@client/v2-events/layouts'
import { TRPCProvider } from '@client/v2-events/trpc'
import AdvancedSearch from '@client/v2-events/features/events/AdvancedSearch/AdvancedSearch'
import { TRPCErrorBoundary } from '@client/v2-events/routes/TRPCErrorBoundary'
import { ROUTES } from './routes'

/**
Expand All @@ -35,10 +37,12 @@ import { ROUTES } from './routes'
export const routesConfig = {
path: ROUTES.V2.path,
element: (
<TRPCProvider>
<Outlet />
<Debug />
</TRPCProvider>
<TRPCErrorBoundary>
<TRPCProvider>
<Outlet />
<Debug />
</TRPCProvider>
</TRPCErrorBoundary>
),
children: [
workqueueRouter,
Expand Down
Loading