Skip to content

Commit fc3ede0

Browse files
authored
feat: exponential backoff retry for GitHub API calls (JhaSourav07#86)
## Description Fixes JhaSourav07#50 Adds a `fetchWithRetry` utility that wraps all GitHub API `fetch` calls with exponential backoff. Retries only on network errors, 429 (rate limit), and 5xx server errors — up to 3 times with 500ms → 1s → 2s delays. All 4xx client errors (401, 403, 404, 422, etc.) return immediately without retrying to avoid duplicate requests on non-transient failures. ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview No SVG visual changes — this is a backend reliability improvement. The badge output is identical; the fix improves resilience when GitHub API returns transient 502/503 errors or drops connections. **Retry behaviour:** | Condition | Action | |---|---| | Network error | Retry with backoff (500ms → 1s → 2s) | | 5xx server error | Retry with backoff | | 429 Too Many Requests | Retry with backoff | | 4xx client error (401, 403, 404, 422 …) | Return immediately — no retry | | 2xx success | Return immediately | ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts).
2 parents 2c9e8bc + 935abc2 commit fc3ede0

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)