Skip to content

Commit 8a50ac5

Browse files
fix: improve GitHub rate limit error handling (JhaSourav07#557)
1 parent 2cc4b15 commit 8a50ac5

2 files changed

Lines changed: 113 additions & 19 deletions

File tree

app/contributors/page.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,37 @@ interface Contributor {
1313
html_url: string;
1414
}
1515

16+
function getRateLimitResetMessage(res: Response): string {
17+
const reset = res.headers.get('x-ratelimit-reset');
18+
19+
if (!reset) {
20+
return '';
21+
}
22+
const resetTimestamp = parseInt(reset, 10);
23+
24+
if (!Number.isFinite(resetTimestamp)) {
25+
return '';
26+
}
27+
const resetAt = new Date(resetTimestamp * 1000).toISOString();
28+
return ` Please try again after ${resetAt}.`;
29+
}
30+
1631
async function getContributors(): Promise<Contributor[]> {
1732
try {
1833
const res = await fetch('https://api.github.com/repos/JhaSourav07/commitpulse/contributors', {
1934
next: { revalidate: 3600 },
2035
});
2136

2237
if (!res.ok) {
23-
return [];
38+
const remaining = res.headers.get('x-ratelimit-remaining');
39+
40+
if ((res.status === 403 && remaining === '0') || res.status === 429) {
41+
throw new Error(
42+
`GitHub API rate limit exceeded.${getRateLimitResetMessage(res)} Please try again later.`
43+
);
44+
}
45+
46+
throw new Error('Failed to fetch contributors');
2447
}
2548

2649
return res.json();

lib/github.ts

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,53 @@ export async function fetchWithRetry(
6161

6262
const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';
6363
const GITHUB_REST_URL = 'https://api.github.com';
64-
const MISSING_GITHUB_TOKEN_MESSAGE = 'GitHub token is missing. Set GITHUB_PAT or GITHUB_TOKEN.';
64+
type GitHubRateLimitInfo = {
65+
limit: number | null;
66+
remaining: number | null;
67+
reset: number | null;
68+
resetAt: string | null;
69+
};
70+
71+
function parseRateLimitHeader(value: string | null): number | null {
72+
if (!value) return null;
73+
74+
const parsed = parseInt(value, 10);
75+
return Number.isFinite(parsed) ? parsed : null;
76+
}
77+
78+
function getGitHubRateLimitInfo(res: Response): GitHubRateLimitInfo {
79+
const limit = parseRateLimitHeader(res.headers.get('x-ratelimit-limit'));
80+
const remaining = parseRateLimitHeader(res.headers.get('x-ratelimit-remaining'));
81+
const reset = parseRateLimitHeader(res.headers.get('x-ratelimit-reset'));
82+
83+
return {
84+
limit,
85+
remaining,
86+
reset,
87+
resetAt: reset ? new Date(reset * 1000).toISOString() : null,
88+
};
89+
}
90+
91+
function createRateLimitError(res: Response): Error {
92+
const rateLimit = getGitHubRateLimitInfo(res);
93+
const resetMessage = rateLimit.resetAt ? ` Please try again after ${rateLimit.resetAt}.` : '';
94+
95+
return new Error(
96+
`GitHub API rate limit exceeded.${resetMessage} Configure GITHUB_TOKEN to increase the request limit.`
97+
);
98+
}
99+
100+
function throwIfRateLimited(res: Response): void {
101+
const rateLimit = getGitHubRateLimitInfo(res);
102+
103+
if (res.status === 403 && rateLimit.remaining === 0) {
104+
throw createRateLimitError(res);
105+
}
106+
107+
if (res.status === 429) {
108+
throw createRateLimitError(res);
109+
}
110+
}
65111

66112
type GitHubContributionResponse = {
67113
data?: {
@@ -127,7 +173,11 @@ export function clearGitHubApiCacheForTests(): void {
127173

128174
function getGitHubToken(): string {
129175
const token = process.env.GITHUB_PAT || process.env.GITHUB_TOKEN;
130-
if (!token || token.trim() === '') throw new Error(MISSING_GITHUB_TOKEN_MESSAGE);
176+
const MISSING_GITHUB_TOKEN_MESSAGE = 'GitHub token is missing. Set GITHUB_PAT or GITHUB_TOKEN.';
177+
if (!token || token.trim() === '') {
178+
throw new Error(MISSING_GITHUB_TOKEN_MESSAGE);
179+
}
180+
131181
return token;
132182
}
133183

@@ -190,6 +240,7 @@ export async function fetchGitHubContributions(
190240
});
191241

192242
if (!res.ok) {
243+
throwIfRateLimited(res);
193244
if (res.status === 401) throw new Error('GitHub PAT is invalid or missing');
194245
throw new Error(`GitHub GraphQL API returned status ${res.status}`);
195246
}
@@ -252,6 +303,7 @@ export async function fetchUserProfile(
252303
});
253304

254305
if (!res.ok) {
306+
throwIfRateLimited(res);
255307
if (res.status === 404) throw new Error('User not found');
256308
throw new Error(`GitHub REST API error: ${res.status}`);
257309
}
@@ -270,9 +322,8 @@ export async function fetchUserRepos(
270322
const cached = reposCache.get(key);
271323
if (cached) return cached;
272324
}
273-
const allRepos: GitHubRepo[] = [];
274325

275-
const res = await fetchWithRetry(
326+
const firstPageRes = await fetchWithRetry(
276327
`${GITHUB_REST_URL}/users/${username}/repos?per_page=100&page=1&sort=pushed`,
277328
{
278329
headers: getHeaders(),
@@ -281,25 +332,45 @@ export async function fetchUserRepos(
281332
}
282333
);
283334

284-
if (!res.ok) throw new Error(`GitHub REST API error: ${res.status}`);
285-
const firstPageRepos = (await res.json()) as GitHubRepo[];
286-
allRepos.push(...firstPageRepos);
335+
if (!firstPageRes.ok) {
336+
throwIfRateLimited(firstPageRes);
337+
throw new Error(`GitHub REST API error: ${firstPageRes.status}`);
338+
}
339+
340+
const firstPageRepos = (await firstPageRes.json()) as GitHubRepo[];
341+
const allRepos: GitHubRepo[] = [...firstPageRepos];
342+
343+
const MAX_PAGES = 3;
287344

288345
if (firstPageRepos.length === 100) {
289-
const fetchPromises = [2, 3].map((page) =>
290-
fetchWithRetry(
291-
`${GITHUB_REST_URL}/users/${username}/repos?per_page=100&page=${page}&sort=pushed`,
292-
{
293-
headers: getHeaders(),
294-
cache: 'no-store',
295-
signal: options.signal,
296-
}
346+
const remainingPages = Array.from({ length: MAX_PAGES - 1 }, (_, i) => i + 2);
347+
348+
const responses = await Promise.all(
349+
remainingPages.map((page) =>
350+
fetchWithRetry(
351+
`${GITHUB_REST_URL}/users/${username}/repos?per_page=100&page=${page}&sort=pushed`,
352+
{
353+
headers: getHeaders(),
354+
cache: 'no-store',
355+
signal: options.signal,
356+
}
357+
)
297358
)
298359
);
299360

300-
const responses = await Promise.all(fetchPromises);
301-
for (const response of responses) {
302-
if (response.ok) allRepos.push(...((await response.json()) as GitHubRepo[]));
361+
const pagesRepos = await Promise.all(
362+
responses.map(async (response) => {
363+
if (!response.ok) {
364+
throwIfRateLimited(response);
365+
throw new Error(`GitHub REST API error: ${response.status}`);
366+
}
367+
368+
return (await response.json()) as GitHubRepo[];
369+
})
370+
);
371+
372+
for (const repos of pagesRepos) {
373+
allRepos.push(...repos);
303374
}
304375
}
305376

0 commit comments

Comments
 (0)