Skip to content

Commit e8cca4e

Browse files
committed
feat: add fetchWithRetry with exponential backoff and timeouts to all Cal.com API calls
1 parent 4cc46e5 commit e8cca4e

1 file changed

Lines changed: 55 additions & 9 deletions

File tree

apps/chat/lib/calcom/client.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import type {
2020
const CALCOM_API_URL = process.env.CALCOM_API_URL ?? "https://api.cal.com";
2121
const API_VERSION = "2024-08-13";
2222

23+
const FETCH_TIMEOUT_MS = 10_000;
24+
const MAX_RETRIES = 2;
25+
const RETRY_BASE_MS = 500;
26+
const RETRY_MULTIPLIER = 3;
27+
const RETRYABLE_STATUS_CODES = new Set([500, 502, 503, 504]);
28+
2329
export class CalcomApiError extends Error {
2430
constructor(
2531
message: string,
@@ -31,22 +37,62 @@ export class CalcomApiError extends Error {
3137
}
3238
}
3339

40+
async function fetchWithRetry(
41+
url: string,
42+
init: RequestInit = {},
43+
maxRetries: number = MAX_RETRIES
44+
): Promise<Response> {
45+
let lastError: Error | undefined;
46+
47+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
48+
try {
49+
const res = await fetch(url, {
50+
...init,
51+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
52+
});
53+
54+
if (RETRYABLE_STATUS_CODES.has(res.status) && attempt < maxRetries) {
55+
await sleep(RETRY_BASE_MS * RETRY_MULTIPLIER ** attempt);
56+
continue;
57+
}
58+
59+
return res;
60+
} catch (err) {
61+
lastError = err instanceof Error ? err : new Error(String(err));
62+
if (attempt < maxRetries) {
63+
await sleep(RETRY_BASE_MS * RETRY_MULTIPLIER ** attempt);
64+
}
65+
}
66+
}
67+
68+
throw new CalcomApiError(
69+
`Cal.com API request failed after ${maxRetries + 1} attempts: ${lastError?.message ?? "unknown error"}`,
70+
undefined,
71+
"FETCH_RETRY_EXHAUSTED"
72+
);
73+
}
74+
75+
function sleep(ms: number): Promise<void> {
76+
return new Promise((resolve) => setTimeout(resolve, ms));
77+
}
78+
3479
async function calcomFetch<T>(
3580
path: string,
3681
accessToken: string,
3782
options: RequestInit = {},
38-
apiVersion: string = API_VERSION
83+
apiVersion: string = API_VERSION,
84+
retries: number = MAX_RETRIES
3985
): Promise<T> {
4086
const url = `${CALCOM_API_URL}${path}`;
41-
const res = await fetch(url, {
87+
const res = await fetchWithRetry(url, {
4288
...options,
4389
headers: {
4490
"cal-api-version": apiVersion,
4591
Authorization: `Bearer ${accessToken}`,
4692
"Content-Type": "application/json",
4793
...options.headers,
4894
},
49-
});
95+
}, retries);
5096

5197
if (!res.ok) {
5298
let errorMessage = `Cal.com API error: ${res.status} ${res.statusText}`;
@@ -147,7 +193,7 @@ export async function getAvailableSlotsPublic(
147193
...(params.bookingUidToReschedule ? { bookingUidToReschedule: params.bookingUidToReschedule } : {}),
148194
});
149195
const url = `${CALCOM_API_URL}/v2/slots?${query}`;
150-
const res = await fetch(url, {
196+
const res = await fetchWithRetry(url, {
151197
headers: {
152198
"cal-api-version": "2024-09-04",
153199
"Content-Type": "application/json",
@@ -232,21 +278,21 @@ export async function createBooking(
232278
return calcomFetch<CalcomBooking>("/v2/bookings", accessToken, {
233279
method: "POST",
234280
body: JSON.stringify(input),
235-
});
281+
}, API_VERSION, 0);
236282
}
237283

238284
export async function createBookingPublic(
239285
input: CreatePublicBookingInput
240286
): Promise<CalcomBooking> {
241287
const url = `${CALCOM_API_URL}/v2/bookings`;
242-
const res = await fetch(url, {
288+
const res = await fetchWithRetry(url, {
243289
method: "POST",
244290
headers: {
245291
"cal-api-version": "2024-08-13",
246292
"Content-Type": "application/json",
247293
},
248294
body: JSON.stringify(input),
249-
});
295+
}, 0);
250296
if (!res.ok) {
251297
const body = await res.text();
252298
let message = `Booking failed (${res.status})`;
@@ -471,14 +517,14 @@ export async function addBookingAttendee(
471517
await calcomFetch<unknown>(`/v2/bookings/${bookingUid}/attendees`, accessToken, {
472518
method: "POST",
473519
body: JSON.stringify(input),
474-
});
520+
}, API_VERSION, 0);
475521
}
476522

477523
// ─── Public event types (no auth) ─────────────────────────────────────────────
478524

479525
export async function getEventTypesByUsername(username: string): Promise<CalcomEventType[]> {
480526
const url = `${CALCOM_API_URL}/v2/event-types?username=${encodeURIComponent(username)}`;
481-
const res = await fetch(url, {
527+
const res = await fetchWithRetry(url, {
482528
headers: {
483529
"cal-api-version": "2024-06-14",
484530
"Content-Type": "application/json",

0 commit comments

Comments
 (0)