diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts index 809b290..e63172e 100644 --- a/app/api/compare/route.ts +++ b/app/api/compare/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { fetchGitHubUserData } from "../../../lib/github"; +import { fetchGitHubUserData, RateLimitError } from "../../../lib/github"; import { calculateUserScore } from "../../../lib/score"; export const runtime = "nodejs"; @@ -36,6 +36,21 @@ export async function GET(request: Request) { return NextResponse.json({ success: true, users: results }); } catch (error: any) { console.error("GitHub score error:", error); + + if (error instanceof RateLimitError) { + const resetMsg = error.resetAt + ? ` Try again after ${error.resetAt.toLocaleTimeString()}.` + : " Please try again later."; + return NextResponse.json( + { + success: false, + error: `GitHub API rate limit exceeded.${resetMsg}`, + rateLimitReset: error.resetAt?.toISOString() ?? null, + }, + { status: 429 } + ); + } + const message = error?.message === "User not found" ? "GitHub user not found" diff --git a/app/page.tsx b/app/page.tsx index a1b5596..a55e7af 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,11 +10,13 @@ type ApiResponse = { success: boolean; users?: UserResult[]; error?: string; + rateLimitReset?: string | null; }; export default function HomePage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [isRateLimit, setIsRateLimit] = useState(false); const [data, setData] = useState<{ user1: UserResult; user2: UserResult; @@ -23,6 +25,7 @@ export default function HomePage() { const handleCompare = async (u1: string, u2: string) => { setLoading(true); setError(null); + setIsRateLimit(false); setData(null); try { const params = new URLSearchParams(); @@ -30,6 +33,10 @@ export default function HomePage() { params.append("username", u2); const res = await fetch(`/api/compare?${params.toString()}`); const body: ApiResponse = await res.json(); + if (res.status === 429 || body.rateLimitReset !== undefined) { + setIsRateLimit(true); + throw new Error(body.error || "GitHub API rate limit exceeded. Please try again later."); + } if (!body.success || !body.users || body.users.length < 2) { throw new Error(body.error || "Comparison failed"); } @@ -58,6 +65,7 @@ export default function HomePage() { const reset = () => { setData(null); setError(null); + setIsRateLimit(false); }; const swapUsers = () => { if (!data) return; @@ -88,7 +96,8 @@ export default function HomePage() { {loading && skeleton} {error && ( -
+
+ {isRateLimit && ⏳ Rate limit reached — } {error}
)} diff --git a/lib/github.ts b/lib/github.ts index 4023aac..9edcdfe 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -57,18 +57,50 @@ const QUERY = /* GraphQL */ ` } `; +export class RateLimitError extends Error { + resetAt?: Date; + constructor(message: string, resetAt?: Date) { + super(message); + this.name = "RateLimitError"; + this.resetAt = resetAt; + } +} + +function parseResetTime(headers: Record): Date | undefined { + const resetEpoch = headers["x-ratelimit-reset"]; + return resetEpoch ? new Date(parseInt(resetEpoch, 10) * 1000) : undefined; +} + export async function fetchGitHubUserData( username: string ): Promise { - const { user } = await client<{ user: any }>(QUERY, { login: username }); + try { + const { user } = await client<{ user: any }>(QUERY, { login: username }); - if (!user) { - throw new Error("User not found"); - } + if (!user) { + throw new Error("User not found"); + } + + return { + repos: user.repositories.nodes as RepoNode[], + pullRequests: user.pullRequests.nodes as PullRequestNode[], + contributions: user.contributionsCollection as ContributionTotals, + }; + } catch (error: any) { + const message: string = error?.message ?? ""; + const isRateLimit = + error?.status === 429 || + (error?.status === 403 && message.toLowerCase().includes("rate limit")) || + message.toLowerCase().includes("api rate limit exceeded") || + message.toLowerCase().includes("secondary rate limit"); - return { - repos: user.repositories.nodes as RepoNode[], - pullRequests: user.pullRequests.nodes as PullRequestNode[], - contributions: user.contributionsCollection as ContributionTotals, - }; + if (isRateLimit) { + const resetAt = error?.headers + ? parseResetTime(error.headers) + : undefined; + throw new RateLimitError("GitHub API rate limit exceeded", resetAt); + } + + throw error; + } }