Skip to content

Commit 9032fb4

Browse files
committed
Improve background token refresh
1 parent b6ecbbc commit 9032fb4

File tree

3 files changed

+73
-33
lines changed

3 files changed

+73
-33
lines changed

web/src/components/health/healthcheck.tsx

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from "react";
77
import { getSecondsUntilExpiration } from "@/lib/time";
88
import { User } from "@/lib/types";
99
import { mockedRefreshToken, refreshToken } from "./refreshUtils";
10-
import { CUSTOM_REFRESH_URL } from "@/lib/constants";
10+
import { NEXT_PUBLIC_CUSTOM_REFRESH_URL } from "@/lib/constants";
1111

1212
export const HealthCheckBanner = () => {
1313
const { error } = useSWR("/api/health", errorHandlingFetcher);
@@ -35,57 +35,89 @@ export const HealthCheckBanner = () => {
3535
}, [user, updateExpirationTime]);
3636

3737
useEffect(() => {
38-
if (CUSTOM_REFRESH_URL) {
39-
const refreshUrl = CUSTOM_REFRESH_URL;
40-
let refreshTimeoutId: NodeJS.Timeout;
38+
if (NEXT_PUBLIC_CUSTOM_REFRESH_URL) {
39+
const refreshUrl = NEXT_PUBLIC_CUSTOM_REFRESH_URL;
40+
let refreshIntervalId: NodeJS.Timer;
4141
let expireTimeoutId: NodeJS.Timeout;
4242

4343
const attemptTokenRefresh = async () => {
44-
try {
45-
// NOTE: This is a mocked refresh token for testing purposes.
46-
// const refreshTokenData = mockedRefreshToken();
47-
48-
const refreshTokenData = await refreshToken(refreshUrl);
49-
50-
const response = await fetch(
51-
"/api/enterprise-settings/refresh-token",
52-
{
53-
method: "POST",
54-
headers: {
55-
"Content-Type": "application/json",
56-
},
57-
body: JSON.stringify(refreshTokenData),
44+
let retryCount = 0;
45+
const maxRetries = 3;
46+
47+
while (retryCount < maxRetries) {
48+
try {
49+
// NOTE: This is a mocked refresh token for testing purposes.
50+
// const refreshTokenData = mockedRefreshToken();
51+
52+
const refreshTokenData = await refreshToken(refreshUrl);
53+
if (!refreshTokenData) {
54+
throw new Error("Failed to refresh token");
5855
}
59-
);
60-
if (!response.ok) {
61-
throw new Error(`HTTP error! status: ${response.status}`);
62-
}
63-
await new Promise((resolve) => setTimeout(resolve, 4000));
6456

65-
await mutateUser(undefined, { revalidate: true });
66-
updateExpirationTime();
67-
} catch (error) {
68-
console.error("Error refreshing token:", error);
57+
const response = await fetch(
58+
"/api/enterprise-settings/refresh-token",
59+
{
60+
method: "POST",
61+
headers: {
62+
"Content-Type": "application/json",
63+
},
64+
body: JSON.stringify(refreshTokenData),
65+
}
66+
);
67+
if (!response.ok) {
68+
throw new Error(`HTTP error! status: ${response.status}`);
69+
}
70+
await new Promise((resolve) => setTimeout(resolve, 4000));
71+
72+
await mutateUser(undefined, { revalidate: true });
73+
updateExpirationTime();
74+
break; // Success - exit the retry loop
75+
} catch (error) {
76+
console.error(
77+
`Error refreshing token (attempt ${
78+
retryCount + 1
79+
}/${maxRetries}):`,
80+
error
81+
);
82+
retryCount++;
83+
84+
if (retryCount === maxRetries) {
85+
console.error("Max retry attempts reached");
86+
} else {
87+
// Wait before retrying (exponential backoff)
88+
await new Promise((resolve) =>
89+
setTimeout(resolve, Math.pow(2, retryCount) * 1000)
90+
);
91+
}
92+
}
6993
}
7094
};
7195

7296
const scheduleRefreshAndExpire = () => {
7397
if (secondsUntilExpiration !== null) {
74-
const timeUntilRefresh = (secondsUntilExpiration + 0.5) * 1000;
75-
refreshTimeoutId = setTimeout(attemptTokenRefresh, timeUntilRefresh);
98+
const refreshInterval = 60 * 15; // 15 mins
99+
refreshIntervalId = setInterval(
100+
attemptTokenRefresh,
101+
refreshInterval * 1000
102+
);
76103

77104
const timeUntilExpire = (secondsUntilExpiration + 10) * 1000;
78105
expireTimeoutId = setTimeout(() => {
79106
console.debug("Session expired. Setting expired state to true.");
80107
setExpired(true);
81108
}, timeUntilExpire);
109+
110+
// if we're going to timeout before the next refresh, kick off a refresh now!
111+
if (secondsUntilExpiration < refreshInterval) {
112+
attemptTokenRefresh();
113+
}
82114
}
83115
};
84116

85117
scheduleRefreshAndExpire();
86118

87119
return () => {
88-
clearTimeout(refreshTimeoutId);
120+
clearInterval(refreshIntervalId);
89121
clearTimeout(expireTimeoutId);
90122
};
91123
}

web/src/components/health/refreshUtils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,21 @@ export function mockedRefreshToken(): CustomRefreshTokenResponse {
4242

4343
export async function refreshToken(
4444
customRefreshUrl: string
45-
): Promise<CustomRefreshTokenResponse> {
45+
): Promise<CustomRefreshTokenResponse | null> {
4646
try {
4747
console.debug("Sending request to custom refresh URL");
48-
const url = new URL(customRefreshUrl);
48+
// support both absolute and relative
49+
const url = customRefreshUrl.startsWith("http")
50+
? new URL(customRefreshUrl)
51+
: new URL(customRefreshUrl, window.location.origin);
4952
url.searchParams.append("info", "json");
5053
url.searchParams.append("access_token_refresh_interval", "3600");
5154

5255
const response = await fetch(url.toString());
56+
if (!response.ok) {
57+
console.error(`Failed to refresh token: ${await response.text()}`);
58+
return null;
59+
}
5360

5461
return await response.json();
5562
} catch (error) {

web/src/lib/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export const NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN =
4242
export const TOGGLED_CONNECTORS_COOKIE_NAME = "toggled_connectors";
4343

4444
/* Enterprise-only settings */
45-
export const CUSTOM_REFRESH_URL = process.env.NEXT_PUBLIC_CUSTOM_REFRESH_URL;
45+
export const NEXT_PUBLIC_CUSTOM_REFRESH_URL =
46+
process.env.NEXT_PUBLIC_CUSTOM_REFRESH_URL;
4647

4748
// NOTE: this should ONLY be used on the server-side. If used client side,
4849
// it will not be accurate (will always be false).

0 commit comments

Comments
 (0)