Skip to content

Commit 935abc2

Browse files
feat(github): add exponential backoff retry for GitHub API calls
Wraps all fetch calls to the GitHub GraphQL and REST APIs with an exponential backoff retry helper. Retries only on network errors, 429 Too Many Requests, and 5xx server errors (up to 3 attempts with 500ms → 1s → 2s delays). All 4xx client errors return immediately without retrying to avoid duplicate requests on non-transient failures.
1 parent 00cbca3 commit 935abc2

1 file changed

Lines changed: 42 additions & 6 deletions

File tree

lib/github.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,39 @@ interface GitHubRepo {
99
language: string | null;
1010
}
1111

12+
const MAX_RETRIES = 3;
13+
const BASE_DELAY_MS = 500;
14+
15+
/**
16+
* Wraps fetch with exponential backoff retry logic.
17+
* Retries ONLY on network errors, 429 Too Many Requests, and 5xx server errors.
18+
* Returns immediately for all other statuses (2xx success, 3xx redirects, 4xx client errors).
19+
*/
20+
export async function fetchWithRetry(
21+
url: string,
22+
options: RequestInit,
23+
attempt = 0
24+
): Promise<Response> {
25+
let res: Response | null = null;
26+
try {
27+
res = await fetch(url, options);
28+
} catch (err) {
29+
// Network error — retry if attempts remain, otherwise rethrow
30+
if (attempt >= MAX_RETRIES) throw err;
31+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
32+
await new Promise((resolve) => setTimeout(resolve, delay));
33+
return fetchWithRetry(url, options, attempt + 1);
34+
}
35+
36+
// Only retry on 429 or 5xx — all other statuses are returned immediately
37+
const shouldRetry = res.status === 429 || res.status >= 500;
38+
if (!shouldRetry || attempt >= MAX_RETRIES) return res;
39+
40+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
41+
await new Promise((resolve) => setTimeout(resolve, delay));
42+
return fetchWithRetry(url, options, attempt + 1);
43+
}
44+
1245
const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';
1346
const GITHUB_REST_URL = 'https://api.github.com';
1447

@@ -91,7 +124,7 @@ export async function fetchGitHubContributions(
91124
}
92125
`;
93126

94-
const res = await fetch(GITHUB_GRAPHQL_URL, {
127+
const res = await fetchWithRetry(GITHUB_GRAPHQL_URL, {
95128
method: 'POST',
96129
headers: getHeaders(),
97130
body: JSON.stringify({ query, variables: { login: username } }),
@@ -133,7 +166,7 @@ export async function fetchUserProfile(
133166
if (cached) return cached;
134167
}
135168

136-
const res = await fetch(`${GITHUB_REST_URL}/users/${username}`, {
169+
const res = await fetchWithRetry(`${GITHUB_REST_URL}/users/${username}`, {
137170
headers: getHeaders(),
138171
cache: 'no-store',
139172
});
@@ -163,10 +196,13 @@ export async function fetchUserRepos(
163196
if (cached) return cached;
164197
}
165198

166-
const res = await fetch(`${GITHUB_REST_URL}/users/${username}/repos?per_page=100&sort=pushed`, {
167-
headers: getHeaders(),
168-
cache: 'no-store',
169-
});
199+
const res = await fetchWithRetry(
200+
`${GITHUB_REST_URL}/users/${username}/repos?per_page=100&sort=pushed`,
201+
{
202+
headers: getHeaders(),
203+
cache: 'no-store',
204+
}
205+
);
170206

171207
if (!res.ok) {
172208
throw new Error(`GitHub REST API error: ${res.status}`);

0 commit comments

Comments
 (0)