#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 {
Box,
Button,
@@ -9,22 +10,19 @@ import {
} from '@linagora/twake-mui'
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
import ReplayIcon from '@mui/icons-material/Replay'
import { useEffect, useRef } from 'react'
import { push } from 'redux-first-history'
import { useI18n } from 'twake-i18n'
export function Error() {
export const Error: React.FC = () => {
const { t } = useI18n()
const dispatch = useAppDispatch()
const userError = useAppSelector(state => state.user.error)
const calendarError = useAppSelector(state => state.calendars.error)
const initialUserError = useRef(userError)
const initialError = useRef(userError || calendarError)
useEffect(() => {
if (!initialUserError.current) {
dispatch(push('/'))
if (!initialError.current) {
window.location.replace('/')
}
}, [dispatch])
}, [])
const errorMessage = userError || calendarError || t('error.unknown')
@@ -69,7 +67,11 @@ export function Error() {
{t('error.title')}
</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}
</Typography>
@@ -77,7 +79,7 @@ export function Error() {
variant="contained"
color="error"
startIcon={<ReplayIcon />}
onClick={() => window.location.reload()}
onClick={() => (window.location.href = '/')}
sx={{
textTransform: 'none',
fontWeight: 600,
+92 -52
View File
@@ -1,7 +1,17 @@
import { Auth } from '@/features/User/oidcAuth'
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 {
TokenEndpointResponse,
TokenEndpointResponseHelpers
} from 'openid-client'
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
@@ -11,50 +21,40 @@ const RETRY_CONFIG = {
maxDelay: 120000
}
export const api = ky.extend({
prefixUrl: window.CALENDAR_BASE_URL,
retry: {
limit: RETRY_CONFIG.maxRetries,
backoffLimit: RETRY_CONFIG.maxDelay,
delay: attemptCount =>
getRetryDelay(attemptCount - 1, {
initialDelay: RETRY_CONFIG.initialDelay,
maxDelay: RETRY_CONFIG.maxDelay
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
})
},
hooks: {
beforeRequest: [
async request => {
const saved = sessionStorage.getItem('tokenSet')
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}')
: null
const access_token = saved?.access_token
if (access_token) {
request.headers.set('Authorization', `Bearer ${access_token}`)
}
if (MUTATING_METHODS.has(request.method)) {
await assertWebSocketAlive()
}
return request
}
],
beforeRetry: [
({ request, error, retryCount }) => {
console.warn(
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
{
url: request.url,
error: error?.message
}
)
redirectTo(loginurl.redirectTo)
} catch (error) {
isRedirectingToSso = false
console.error('SSO Redirect failed:', error)
throw new HTTPError(response, request, options)
}
],
}
afterResponse: [
async (request, options, response) => {
if (response.status === 401) {
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') {
@@ -68,16 +68,56 @@ export const api = ky.extend({
}
// Only redirect to SSO if we're sure token is invalid (not just missing)
const loginurl = await Auth()
await redirectSSO(response, request, options)
}
sessionStorage.setItem(
'redirectState',
JSON.stringify({
code_verifier: loginurl.code_verifier,
state: loginurl.state
export const api: KyInstance = ky.extend({
prefixUrl: window.CALENDAR_BASE_URL,
retry: {
limit: RETRY_CONFIG.maxRetries,
backoffLimit: RETRY_CONFIG.maxDelay,
delay: attemptCount =>
getRetryDelay(attemptCount - 1, {
initialDelay: RETRY_CONFIG.initialDelay,
maxDelay: RETRY_CONFIG.maxDelay
})
},
hooks: {
beforeRequest: [
async (request: KyRequest): Promise<KyRequest> => {
const saved = sessionStorage.getItem('tokenSet')
? (JSON.parse(
sessionStorage.getItem('tokenSet') ?? '{}'
) as TokenEndpointResponse & TokenEndpointResponseHelpers)
: null
const access_token = saved?.access_token as string
if (access_token) {
request.headers.set('Authorization', `Bearer ${access_token}`)
}
if (MUTATING_METHODS.has(request.method)) {
await assertWebSocketAlive()
}
return request
}
],
beforeRetry: [
({ request, error, retryCount }): void => {
console.warn(
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
{
url: request.url,
error: error?.message
}
)
redirectTo(loginurl.redirectTo)
}
],
afterResponse: [
async (request, options, response): Promise<KyResponse> => {
if (response.status === 401) {
await handleUnauthorizeRequest(response, request, options)
}
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)
}
export function getLocation() {
export function getLocation(): string {
return window.location.href
}
export function isValidUrl(string?: string) {
export function isValidUrl(string?: string): URL | boolean {
let url
try {
@@ -104,7 +144,7 @@ export function isValidUrl(string?: string) {
return url
}
export async function importFile(file: File) {
export async function importFile(file: File): Promise<KyResponse<unknown>> {
const response = await api.post(
`api/files?mimetype=${file.type}&name=${file.name}&size=${file.size}`,
{ body: await file.text() }