[#418] improved retry logic to prevent loading loop (#427)

* [#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:
Camille Moussu
2025-12-19 12:24:34 +01:00
committed by GitHub
parent 90d6058b0f
commit 6839353f34
5 changed files with 70 additions and 9 deletions
+4 -3
View File
@@ -1,7 +1,7 @@
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import ReplayIcon from "@mui/icons-material/Replay";
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 { useAppDispatch, useAppSelector } from "../../app/hooks";
import { useI18n } from "twake-i18n";
@@ -11,12 +11,13 @@ export function Error() {
const dispatch = useAppDispatch();
const userError = useAppSelector((state) => state.user.error);
const calendarError = useAppSelector((state) => state.calendars.error);
const initialUserError = useRef(userError);
useEffect(() => {
if (!userError) {
if (!initialUserError.current) {
dispatch(push("/"));
}
}, [calendarError, dispatch]);
}, [dispatch]);
const errorMessage = userError || calendarError || t("error.unknown");
+13 -2
View File
@@ -59,10 +59,21 @@ export function HandleLogin() {
if (userData.error) {
dispatch(push("/error"));
}
if (!calendars.pending && !userData.loading && !userData.error) {
if (
!calendars.pending &&
!userData.loading &&
!userData.error &&
!calendars.error
) {
dispatch(push("/calendar"));
}
}, [calendars.pending, userData.loading, userData.error, dispatch]);
}, [
calendars.pending,
userData.loading,
userData.error,
calendars.error,
dispatch,
]);
return <Loading />;
}
+15 -2
View File
@@ -1,8 +1,13 @@
import { useEffect, useRef } from "react";
import { Callback } from "./oidcAuth";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { useAppDispatch } from "../../app/hooks";
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 { getCalendarsListAsync } from "../Calendars/CalendarSlice";
@@ -21,6 +26,9 @@ export function CallbackResume() {
const runCallback = async () => {
try {
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(setTokens(data?.tokenSet));
await dispatch(getOpenPaasUserDataAsync());
@@ -37,6 +45,11 @@ export function CallbackResume() {
dispatch(push("/"));
} catch (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"));
}
};
+4
View File
@@ -86,6 +86,9 @@ export const userSlice = createSlice({
setAlarmEmails: (state, action) => {
state.alarmEmailsEnabled = action.payload;
},
setUserError: (state, action) => {
state.error = action.payload;
},
clearError: (state) => {
state.error = null;
},
@@ -218,6 +221,7 @@ export const {
setLanguage,
setTimezone,
setAlarmEmails,
setUserError,
clearError,
} = userSlice.actions;
+34 -2
View File
@@ -1,8 +1,26 @@
import ky from "ky";
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({
prefixUrl: (window as any).CALENDAR_BASE_URL,
retry: {
limit: RETRY_CONFIG.maxRetries,
backoffLimit: RETRY_CONFIG.maxDelay,
delay: (attemptCount) => getRetryDelay(attemptCount - 1),
},
hooks: {
beforeRequest: [
async (request) => {
@@ -10,11 +28,25 @@ export const api = ky.extend({
? JSON.parse(sessionStorage.getItem("tokenSet")!)
: null;
const access_token = saved?.access_token;
request.headers.set("Authorization", `Bearer ${access_token}`);
if (access_token) {
request.headers.set("Authorization", `Bearer ${access_token}`);
}
return request;
},
],
beforeRetry: [
async ({ request, error, retryCount }) => {
console.warn(
`[API Retry] Attempt ${retryCount}/${RETRY_CONFIG.maxRetries}`,
{
url: request.url,
error: error?.message,
}
);
},
],
afterResponse: [
async (request, options, response) => {
if (response.status === 401) {