Skip to content

Commit 302f1f3

Browse files
authored
fix(api): classify compare route GitHub errors (JhaSourav07#2205)
## Description Fixes JhaSourav07#2204 ## Pillar - [x] 🛠️ Other (Bug fix, refactoring, docs) ## What changed The `/api/compare` route used `Promise.allSettled`, but both rejected fetch branches returned `404` unconditionally. That made GitHub rate limits and upstream failures look like missing users. This PR adds a small helper that classifies rejected GitHub dashboard fetches before responding: - not-found / could-not-resolve errors still return `404` - rate-limit style errors return `403` with the existing GitHub rate-limit message - other upstream failures return `500` instead of being mislabeled as not-found Tests were added for user1 rate-limit, user2 rate-limit, and a non-rate-limit upstream failure. ## Visual Preview N/A - JSON API status-code fix only. ## Local verification - `npm run test -- app/api/compare/route.test.ts` passed - `npm run format:check` passed - `npm run lint` passed with one existing warning in `components/WallOfLove.tsx` - `npm run typecheck` passed - `npm run test` passed: 83 files, 1438 passed, 1 expected fail - `npm run build` fails locally with an opaque webpack error; this appears environment/upstream-local because CI build passed on the prior PR after the same local failure pattern ## GSSoC 2026 This is raised and fixed under GSSoC 2026. Please add the relevant GSSoC/level labels if they do not sync from the issue automatically. ## 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` / `npm run format:check` and `npm run lint` locally. - [x] I have run `npm run test` and all tests pass locally. - [x] My commits follow the Conventional Commits format. - [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 quality standard.
2 parents db3161a + a0ed34b commit 302f1f3

2 files changed

Lines changed: 68 additions & 12 deletions

File tree

app/api/compare/route.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,38 @@ describe('GET /api/compare', () => {
9090
const res = await GET(makeRequest('user1=octocat&user2=ghost123'));
9191
expect(res.status).toBe(404);
9292
});
93+
94+
it('returns 403 when the user1 fetch hits a GitHub rate limit', async () => {
95+
vi.mocked(getFullDashboardData).mockRejectedValueOnce(new Error('API Rate Limit Exceeded'));
96+
97+
const res = await GET(makeRequest('user1=octocat&user2=torvalds'));
98+
99+
expect(res.status).toBe(403);
100+
const data = await res.json();
101+
expect(data.error).toBe('GitHub API rate limit reached. Please configure GITHUB_TOKEN.');
102+
});
103+
104+
it('returns 403 when the user2 fetch hits a GitHub rate limit', async () => {
105+
vi.mocked(getFullDashboardData)
106+
.mockResolvedValueOnce({ calendar: { totalContributions: 0, weeks: [] } } as never)
107+
.mockRejectedValueOnce(new Error('GitHub GraphQL API returned status 403'));
108+
109+
const res = await GET(makeRequest('user1=octocat&user2=torvalds'));
110+
111+
expect(res.status).toBe(403);
112+
const data = await res.json();
113+
expect(data.error).toBe('GitHub API rate limit reached. Please configure GITHUB_TOKEN.');
114+
});
115+
116+
it('returns 500 for non-rate-limit upstream failures instead of reporting not found', async () => {
117+
vi.mocked(getFullDashboardData).mockRejectedValueOnce(
118+
new Error('GitHub GraphQL API returned status 500')
119+
);
120+
121+
const res = await GET(makeRequest('user1=octocat&user2=torvalds'));
122+
123+
expect(res.status).toBe(500);
124+
const data = await res.json();
125+
expect(data.error).toContain('GitHub GraphQL API returned status 500');
126+
});
93127
});

app/api/compare/route.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@ import { compareParamsSchema } from '@/lib/validations';
44

55
export const revalidate = 3600;
66

7+
function buildCompareFetchErrorResponse(user: string, reason: unknown): NextResponse {
8+
const message = reason instanceof Error ? reason.message : 'Unknown error';
9+
const lowerMessage = message.toLowerCase();
10+
11+
if (lowerMessage.includes('not found') || lowerMessage.includes('could not resolve')) {
12+
return NextResponse.json(
13+
{
14+
error: `Failed to fetch data for "${user}": ${message}`,
15+
},
16+
{ status: 404 }
17+
);
18+
}
19+
20+
if (
21+
lowerMessage.includes('rate limit') ||
22+
message.includes('API limit reached') ||
23+
message.includes('status 403')
24+
) {
25+
return NextResponse.json(
26+
{ error: 'GitHub API rate limit reached. Please configure GITHUB_TOKEN.' },
27+
{ status: 403 }
28+
);
29+
}
30+
31+
return NextResponse.json(
32+
{
33+
error: `Failed to fetch data for "${user}": ${message}`,
34+
},
35+
{ status: 500 }
36+
);
37+
}
38+
739
export async function GET(request: Request) {
840
const { searchParams } = new URL(request.url);
941

@@ -26,21 +58,11 @@ export async function GET(request: Request) {
2658
]);
2759

2860
if (result1.status === 'rejected') {
29-
return NextResponse.json(
30-
{
31-
error: `Failed to fetch data for "${user1}": ${result1.reason?.message || 'Unknown error'}`,
32-
},
33-
{ status: 404 }
34-
);
61+
return buildCompareFetchErrorResponse(user1, result1.reason);
3562
}
3663

3764
if (result2.status === 'rejected') {
38-
return NextResponse.json(
39-
{
40-
error: `Failed to fetch data for "${user2}": ${result2.reason?.message || 'Unknown error'}`,
41-
},
42-
{ status: 404 }
43-
);
65+
return buildCompareFetchErrorResponse(user2, result2.reason);
4466
}
4567

4668
return NextResponse.json({

0 commit comments

Comments
 (0)