#623 handle white page crash (#824)

Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
lethemanh
2026-04-27 15:27:15 +07:00
committed by GitHub
parent dffb305886
commit 4179c274fc
2 changed files with 87 additions and 45 deletions
+13 -11
View File
@@ -1,4 +1,5 @@
import { useAppDispatch, useAppSelector } from '@/app/hooks' import { useEffect, useRef } from 'react'
import { useAppSelector } from '@/app/hooks'
import { import {
Box, Box,
Button, Button,
@@ -9,22 +10,19 @@ import {
} from '@linagora/twake-mui' } from '@linagora/twake-mui'
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
import ReplayIcon from '@mui/icons-material/Replay' import ReplayIcon from '@mui/icons-material/Replay'
import { useEffect, useRef } from 'react'
import { push } from 'redux-first-history'
import { useI18n } from 'twake-i18n' import { useI18n } from 'twake-i18n'
export function Error() { export const Error: React.FC = () => {
const { t } = useI18n() const { t } = useI18n()
const dispatch = useAppDispatch()
const userError = useAppSelector(state => state.user.error) const userError = useAppSelector(state => state.user.error)
const calendarError = useAppSelector(state => state.calendars.error) const calendarError = useAppSelector(state => state.calendars.error)
const initialUserError = useRef(userError) const initialError = useRef(userError || calendarError)
useEffect(() => { useEffect(() => {
if (!initialUserError.current) { if (!initialError.current) {
dispatch(push('/')) window.location.replace('/')
} }
}, [dispatch]) }, [])
const errorMessage = userError || calendarError || t('error.unknown') const errorMessage = userError || calendarError || t('error.unknown')
@@ -69,7 +67,11 @@ export function Error() {
{t('error.title')} {t('error.title')}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}> <Typography
variant="body1"
color="text.secondary"
sx={{ mb: 2, width: '100%', wordBreak: 'break-all' }}
>
{errorMessage} {errorMessage}
</Typography> </Typography>
@@ -77,7 +79,7 @@ export function Error() {
variant="contained" variant="contained"
color="error" color="error"
startIcon={<ReplayIcon />} startIcon={<ReplayIcon />}
onClick={() => window.location.reload()} onClick={() => (window.location.href = '/')}
sx={{ sx={{
textTransform: 'none', textTransform: 'none',
fontWeight: 600, fontWeight: 600,
+74 -34
View File
@@ -1,7 +1,17 @@
import { Auth } from '@/features/User/oidcAuth' import { Auth } from '@/features/User/oidcAuth'
import { assertWebSocketAlive } from '@/websocket/connection/lifecycle/assertWebSocketAlive' import { assertWebSocketAlive } from '@/websocket/connection/lifecycle/assertWebSocketAlive'
import ky from 'ky' import ky, {
type KyInstance,
type KyRequest,
type KyResponse,
HTTPError,
type NormalizedOptions
} from 'ky'
import { getRetryDelay } from './getRetryDelay' import { getRetryDelay } from './getRetryDelay'
import {
TokenEndpointResponse,
TokenEndpointResponseHelpers
} from 'openid-client'
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']) const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
@@ -11,7 +21,57 @@ const RETRY_CONFIG = {
maxDelay: 120000 maxDelay: 120000
} }
export const api = ky.extend({ let isRedirectingToSso = false
const redirectSSO = async (
response: KyResponse,
request: KyRequest,
options: NormalizedOptions
): Promise<void> => {
if (isRedirectingToSso) {
return
}
isRedirectingToSso = true
try {
const loginurl = await Auth()
sessionStorage.setItem(
'redirectState',
JSON.stringify({
code_verifier: loginurl.code_verifier,
state: loginurl.state
})
)
redirectTo(loginurl.redirectTo)
} catch (error) {
isRedirectingToSso = false
console.error('SSO Redirect failed:', error)
throw new HTTPError(response, request, options)
}
}
const handleUnauthorizeRequest = async (
response: KyResponse,
request: KyRequest,
options: NormalizedOptions
): Promise<KyResponse | void> => {
// Check if we're already on login flow to prevent redirect loop
const currentPath = window.location.pathname
if (currentPath === '/callback') {
return response
}
// Check if we have a token in the request
const hasAuthHeader = request.headers.has('Authorization')
if (!hasAuthHeader) {
return response
}
// Only redirect to SSO if we're sure token is invalid (not just missing)
await redirectSSO(response, request, options)
}
export const api: KyInstance = ky.extend({
prefixUrl: window.CALENDAR_BASE_URL, prefixUrl: window.CALENDAR_BASE_URL,
retry: { retry: {
limit: RETRY_CONFIG.maxRetries, limit: RETRY_CONFIG.maxRetries,
@@ -24,11 +84,13 @@ export const api = ky.extend({
}, },
hooks: { hooks: {
beforeRequest: [ beforeRequest: [
async request => { async (request: KyRequest): Promise<KyRequest> => {
const saved = sessionStorage.getItem('tokenSet') const saved = sessionStorage.getItem('tokenSet')
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}') ? (JSON.parse(
sessionStorage.getItem('tokenSet') ?? '{}'
) as TokenEndpointResponse & TokenEndpointResponseHelpers)
: null : null
const access_token = saved?.access_token const access_token = saved?.access_token as string
if (access_token) { if (access_token) {
request.headers.set('Authorization', `Bearer ${access_token}`) request.headers.set('Authorization', `Bearer ${access_token}`)
} }
@@ -41,7 +103,7 @@ export const api = ky.extend({
], ],
beforeRetry: [ beforeRetry: [
({ request, error, retryCount }) => { ({ request, error, retryCount }): void => {
console.warn( console.warn(
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`, `[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
{ {
@@ -53,31 +115,9 @@ export const api = ky.extend({
], ],
afterResponse: [ afterResponse: [
async (request, options, response) => { async (request, options, response): Promise<KyResponse> => {
if (response.status === 401) { if (response.status === 401) {
// Check if we're already on login flow to prevent redirect loop await handleUnauthorizeRequest(response, request, options)
const currentPath = window.location.pathname
if (currentPath === '/callback') {
return response
}
// Check if we have a token in the request
const hasAuthHeader = request.headers.has('Authorization')
if (!hasAuthHeader) {
return response
}
// Only redirect to SSO if we're sure token is invalid (not just missing)
const loginurl = await Auth()
sessionStorage.setItem(
'redirectState',
JSON.stringify({
code_verifier: loginurl.code_verifier,
state: loginurl.state
})
)
redirectTo(loginurl.redirectTo)
} }
return response return response
} }
@@ -85,15 +125,15 @@ export const api = ky.extend({
} }
}) })
export function redirectTo(url: URL) { export function redirectTo(url: URL): void {
window.location.assign(url) window.location.assign(url)
} }
export function getLocation() { export function getLocation(): string {
return window.location.href return window.location.href
} }
export function isValidUrl(string?: string) { export function isValidUrl(string?: string): URL | boolean {
let url let url
try { try {
@@ -104,7 +144,7 @@ export function isValidUrl(string?: string) {
return url return url
} }
export async function importFile(file: File) { export async function importFile(file: File): Promise<KyResponse<unknown>> {
const response = await api.post( const response = await api.post(
`api/files?mimetype=${file.type}&name=${file.name}&size=${file.size}`, `api/files?mimetype=${file.type}&name=${file.name}&size=${file.size}`,
{ body: await file.text() } { body: await file.text() }