Skip to content

Commit 701ff84

Browse files
committed
feat: handle GitHub API rate limits gracefully
When the GitHub API returns 403/429 (rate limited), the app now: 1. Falls back to stale cached data if available 2. Returns a friendly 429 response with Retry-After header when no cached data exists Adds RateLimitError class and stale cache lookup helper. Closes #32
1 parent 18cba75 commit 701ff84

3 files changed

Lines changed: 62 additions & 13 deletions

File tree

app/api/compare/route.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextResponse } from "next/server";
2-
import { fetchGitHubUserData } from "../../../lib/github";
2+
import { fetchGitHubUserData, RateLimitError } from "../../../lib/github";
33
import { calculateUserScore } from "../../../lib/score";
44

55
export const runtime = "nodejs";
@@ -39,6 +39,21 @@ export async function GET(request: Request) {
3939
);
4040
} catch (error: any) {
4141
console.error("GitHub score error:", error);
42+
43+
if (error instanceof RateLimitError) {
44+
return NextResponse.json(
45+
{
46+
success: false,
47+
error: "GitHub API rate limit exceeded. Please try again later.",
48+
retryAfter: error.retryAfter,
49+
},
50+
{
51+
status: 429,
52+
headers: { "Retry-After": String(error.retryAfter) },
53+
}
54+
);
55+
}
56+
4257
const message =
4358
error?.message === "User not found"
4459
? "GitHub user not found"

lib/cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export function getCached<T>(key: string): T | undefined {
1717
return entry.data as T;
1818
}
1919

20+
export function getStale<T>(key: string): T | undefined {
21+
const entry = store.get(key);
22+
if (!entry) return undefined;
23+
return entry.data as T;
24+
}
25+
2026
export function setCached<T>(key: string, data: T, ttlMs = DEFAULT_TTL_MS) {
2127
store.set(key, { data, expiresAt: Date.now() + ttlMs });
2228
}

lib/github.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ContributionTotals, GitHubUserData, PullRequestNode, RepoNode } from "@/types/github";
22
import { graphql } from "@octokit/graphql";
3-
import { getCached, setCached } from "./cache";
3+
import { getCached, getStale, setCached } from "./cache";
44

55
if (!process.env.GITHUB_TOKEN) {
66
throw new Error("Missing GITHUB_TOKEN");
@@ -58,25 +58,53 @@ const QUERY = /* GraphQL */ `
5858
}
5959
`;
6060

61+
export class RateLimitError extends Error {
62+
retryAfter: number;
63+
constructor(retryAfter: number) {
64+
super("GitHub API rate limit exceeded");
65+
this.name = "RateLimitError";
66+
this.retryAfter = retryAfter;
67+
}
68+
}
69+
70+
function isRateLimitError(error: any): boolean {
71+
const status = error?.status ?? error?.response?.status;
72+
return status === 403 || status === 429;
73+
}
74+
6175
export async function fetchGitHubUserData(
6276
username: string
6377
): Promise<GitHubUserData> {
6478
const cacheKey = `github:${username.toLowerCase()}`;
6579
const cached = getCached<GitHubUserData>(cacheKey);
6680
if (cached) return cached;
6781

68-
const { user } = await client<{ user: any }>(QUERY, { login: username });
82+
try {
83+
const { user } = await client<{ user: any }>(QUERY, { login: username });
6984

70-
if (!user) {
71-
throw new Error("User not found");
72-
}
85+
if (!user) {
86+
throw new Error("User not found");
87+
}
88+
89+
const data: GitHubUserData = {
90+
repos: user.repositories.nodes as RepoNode[],
91+
pullRequests: user.pullRequests.nodes as PullRequestNode[],
92+
contributions: user.contributionsCollection as ContributionTotals,
93+
};
7394

74-
const data: GitHubUserData = {
75-
repos: user.repositories.nodes as RepoNode[],
76-
pullRequests: user.pullRequests.nodes as PullRequestNode[],
77-
contributions: user.contributionsCollection as ContributionTotals,
78-
};
95+
setCached(cacheKey, data);
96+
return data;
97+
} catch (error: any) {
98+
if (isRateLimitError(error)) {
99+
const stale = getStale<GitHubUserData>(cacheKey);
100+
if (stale) return stale;
79101

80-
setCached(cacheKey, data);
81-
return data;
102+
const resetAt = error.response?.headers?.["x-ratelimit-reset"];
103+
const retryAfter = resetAt
104+
? Math.max(0, Number(resetAt) - Math.floor(Date.now() / 1000))
105+
: 60;
106+
throw new RateLimitError(retryAfter);
107+
}
108+
throw error;
109+
}
82110
}

0 commit comments

Comments
 (0)