* [#418] improved retry logic to prevent loading loop * [#418] added FullJitter algorithm Co-authored-by: Benoit TELLIER <btellier@linagora.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
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 { Box, Button, Fade, Paper, Stack, Typography } from "@mui/material";
|
import { Box, Button, Fade, Paper, Stack, Typography } from "@mui/material";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
@@ -11,12 +11,13 @@ export function Error() {
|
|||||||
const dispatch = useAppDispatch();
|
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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userError) {
|
if (!initialUserError.current) {
|
||||||
dispatch(push("/"));
|
dispatch(push("/"));
|
||||||
}
|
}
|
||||||
}, [calendarError, dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const errorMessage = userError || calendarError || t("error.unknown");
|
const errorMessage = userError || calendarError || t("error.unknown");
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,21 @@ export function HandleLogin() {
|
|||||||
if (userData.error) {
|
if (userData.error) {
|
||||||
dispatch(push("/error"));
|
dispatch(push("/error"));
|
||||||
}
|
}
|
||||||
if (!calendars.pending && !userData.loading && !userData.error) {
|
if (
|
||||||
|
!calendars.pending &&
|
||||||
|
!userData.loading &&
|
||||||
|
!userData.error &&
|
||||||
|
!calendars.error
|
||||||
|
) {
|
||||||
dispatch(push("/calendar"));
|
dispatch(push("/calendar"));
|
||||||
}
|
}
|
||||||
}, [calendars.pending, userData.loading, userData.error, dispatch]);
|
}, [
|
||||||
|
calendars.pending,
|
||||||
|
userData.loading,
|
||||||
|
userData.error,
|
||||||
|
calendars.error,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Callback } from "./oidcAuth";
|
import { Callback } from "./oidcAuth";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch } from "../../app/hooks";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import { getOpenPaasUserDataAsync, setTokens, setUserData } from "./userSlice";
|
import {
|
||||||
|
getOpenPaasUserDataAsync,
|
||||||
|
setTokens,
|
||||||
|
setUserData,
|
||||||
|
setUserError,
|
||||||
|
} from "./userSlice";
|
||||||
import { Loading } from "../../components/Loading/Loading";
|
import { Loading } from "../../components/Loading/Loading";
|
||||||
import { getCalendarsListAsync } from "../Calendars/CalendarSlice";
|
import { getCalendarsListAsync } from "../Calendars/CalendarSlice";
|
||||||
|
|
||||||
@@ -21,6 +26,9 @@ export function CallbackResume() {
|
|||||||
const runCallback = async () => {
|
const runCallback = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await Callback(saved?.code_verifier, saved?.state);
|
const data = await Callback(saved?.code_verifier, saved?.state);
|
||||||
|
if (!data || !data.userinfo || !data.tokenSet) {
|
||||||
|
throw new Error("OAuth callback failed");
|
||||||
|
}
|
||||||
dispatch(setUserData(data?.userinfo));
|
dispatch(setUserData(data?.userinfo));
|
||||||
dispatch(setTokens(data?.tokenSet));
|
dispatch(setTokens(data?.tokenSet));
|
||||||
await dispatch(getOpenPaasUserDataAsync());
|
await dispatch(getOpenPaasUserDataAsync());
|
||||||
@@ -37,6 +45,11 @@ export function CallbackResume() {
|
|||||||
dispatch(push("/"));
|
dispatch(push("/"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("OIDC callback error:", e);
|
console.error("OIDC callback error:", e);
|
||||||
|
// Redirect to error page after error
|
||||||
|
dispatch(
|
||||||
|
setUserError(e instanceof Error ? e.message : "OAuth callback failed")
|
||||||
|
);
|
||||||
|
dispatch(push("/error"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ export const userSlice = createSlice({
|
|||||||
setAlarmEmails: (state, action) => {
|
setAlarmEmails: (state, action) => {
|
||||||
state.alarmEmailsEnabled = action.payload;
|
state.alarmEmailsEnabled = action.payload;
|
||||||
},
|
},
|
||||||
|
setUserError: (state, action) => {
|
||||||
|
state.error = action.payload;
|
||||||
|
},
|
||||||
clearError: (state) => {
|
clearError: (state) => {
|
||||||
state.error = null;
|
state.error = null;
|
||||||
},
|
},
|
||||||
@@ -218,6 +221,7 @@ export const {
|
|||||||
setLanguage,
|
setLanguage,
|
||||||
setTimezone,
|
setTimezone,
|
||||||
setAlarmEmails,
|
setAlarmEmails,
|
||||||
|
setUserError,
|
||||||
clearError,
|
clearError,
|
||||||
} = userSlice.actions;
|
} = userSlice.actions;
|
||||||
|
|
||||||
|
|||||||
+34
-2
@@ -1,8 +1,26 @@
|
|||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
import { Auth } from "../features/User/oidcAuth";
|
import { Auth } from "../features/User/oidcAuth";
|
||||||
|
|
||||||
|
const RETRY_CONFIG = {
|
||||||
|
maxRetries: 10,
|
||||||
|
initialDelay: 1000,
|
||||||
|
maxDelay: 120000,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRetryDelay(attemptNumber: number): number {
|
||||||
|
const cap = RETRY_CONFIG.maxDelay;
|
||||||
|
const base = RETRY_CONFIG.initialDelay * Math.pow(2, attemptNumber);
|
||||||
|
const jitter = 0.5 + Math.random();
|
||||||
|
return Math.min(cap, base * jitter);
|
||||||
|
}
|
||||||
|
|
||||||
export const api = ky.extend({
|
export const api = ky.extend({
|
||||||
prefixUrl: (window as any).CALENDAR_BASE_URL,
|
prefixUrl: (window as any).CALENDAR_BASE_URL,
|
||||||
|
retry: {
|
||||||
|
limit: RETRY_CONFIG.maxRetries,
|
||||||
|
backoffLimit: RETRY_CONFIG.maxDelay,
|
||||||
|
delay: (attemptCount) => getRetryDelay(attemptCount - 1),
|
||||||
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeRequest: [
|
beforeRequest: [
|
||||||
async (request) => {
|
async (request) => {
|
||||||
@@ -10,11 +28,25 @@ export const api = ky.extend({
|
|||||||
? JSON.parse(sessionStorage.getItem("tokenSet")!)
|
? JSON.parse(sessionStorage.getItem("tokenSet")!)
|
||||||
: null;
|
: null;
|
||||||
const access_token = saved?.access_token;
|
const access_token = saved?.access_token;
|
||||||
|
if (access_token) {
|
||||||
request.headers.set("Authorization", `Bearer ${access_token}`);
|
request.headers.set("Authorization", `Bearer ${access_token}`);
|
||||||
|
}
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
beforeRetry: [
|
||||||
|
async ({ request, error, retryCount }) => {
|
||||||
|
console.warn(
|
||||||
|
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
|
||||||
|
{
|
||||||
|
url: request.url,
|
||||||
|
error: error?.message,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
afterResponse: [
|
afterResponse: [
|
||||||
async (request, options, response) => {
|
async (request, options, response) => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
|||||||
Reference in New Issue
Block a user