Skip to content

fix(api): return fallback SVG and 429 on GitHub GraphQL rate-limit ex…#5802

Open
Payal-mak wants to merge 2 commits into
JhaSourav07:mainfrom
Payal-mak:fix/streak-rate-limit-fallback
Open

fix(api): return fallback SVG and 429 on GitHub GraphQL rate-limit ex…#5802
Payal-mak wants to merge 2 commits into
JhaSourav07:mainfrom
Payal-mak:fix/streak-rate-limit-fallback

Conversation

@Payal-mak

Copy link
Copy Markdown

Description

Fixes #5404

The /api/streak endpoint previously had no handling for GitHub GraphQL rate-limit exhaustion — a rate-limited response would surface as an unhandled 500 or blank body, breaking the badge image in READMEs with no recovery path.

This PR brings /api/streak in line with the existing rate-limit/circuit-breaker pattern already used by /api/wrapped:

  • Typed RateLimitError (lib/github.ts): fetchGitHubContributions now detects errors[].type === "RATE_LIMITED" in the GraphQL response body and throws a typed RateLimitError carrying a computed retryAfterMs (derived from the x-ratelimit-reset header, falling back to 60s).
  • Graceful fallback SVG: when rate-limited, /api/streak now returns the shared generateRateLimitSVG badge (matching the existing premium "rate limit / circuit breaker" design from /api/wrapped) instead of an empty body or stack trace.
  • HTTP 429 instead of 500: rate-limited responses now correctly return 429 with Cache-Control: no-store.
  • Retry-After header: when RateLimitError.retryAfterMs is available, buildErrorResponse attaches a Retry-After header (in seconds) so clients and CDNs back off correctly.
  • Circuit breaker telemetry: /api/streak now calls getCircuitTelemetry() to include circuit-open status in the rate-limit response, consistent with /api/wrapped.
  • Proactive quota/IP-refresh handling: leverages the existing quotaMonitor and refreshRateLimiter services so /api/streak returns 429 proactively when GitHub API quota is low or per-IP refresh limits are exceeded — before even attempting a GraphQL call.

Files Changed

  • lib/github.ts — added RateLimitError class (with retryAfterMs), typed throw on body-level RATE_LIMITED
  • app/api/streak/route.tsbuildErrorResponse now handles RateLimitError (429, Retry-After, fallback SVG, circuit telemetry headers)
  • app/api/streak/tests/refresh.test.ts — updated lib/github mock to use importOriginal (preserves real RateLimitError class for instanceof checks) and added getCircuitTelemetry mock
  • app/api/wrapped/route.test.ts — updated one assertion to match the shared SVG's actual rendered text ('rate limit')
  • lib/github.test.ts — added test coverage for RateLimitError class contract and the typed-error path on body-level RATE_LIMITED responses

Pillar

  • 🎨 Pillar 1 — New Theme Design
  • 📐 Pillar 2 — Geometric SVG Improvement
  • 🕐 Pillar 3 — Timezone Logic Optimization
  • 🛠️ Other (Bug fix, refactoring, docs)

Visual Preview

image

then,

image

Checklist before requesting a review:

  • I have read the CONTRIBUTING.md file.
  • I have tested these changes locally (localhost:3000/api/streak?user=YOUR_USERNAME).
  • I have run npm run format and npm run lint locally and resolved all errors (CI will fail otherwise).
  • My commits follow the Conventional Commits format (e.g., feat(themes): ..., fix(calculate): ...).
  • I have updated README.md if I added a new theme or URL parameter. (N/A — no new theme or URL parameter added)
  • I have started the repo. (This means a lot to us ❤️)
  • I have made sure that I have only one commit to merge in this PR.
  • The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts)
  • (Recommended) I joined the CommitPulse Discord server for faster collaboration, mentorship, and PR support.

Copilot AI review requested due to automatic review settings June 16, 2026 02:25
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

@Payal-mak is attempting to deploy a commit to the jhasourav07's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduce a typed RateLimitError with optional backoff metadata so API routes can reliably detect GitHub GraphQL rate-limiting and respond with 429 + Retry-After.

Changes:

  • Add/extend RateLimitError to carry retryAfterMs and throw it on GitHub GraphQL RATE_LIMITED.
  • Attach Retry-After header in the streak API when the error is a RateLimitError.
  • Expand unit/integration tests around typed rate-limit handling and headers.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
lib/github.ts Throws a typed RateLimitError with computed backoff from rate-limit headers.
lib/github.test.ts Adds/updates tests asserting RateLimitError behavior and backoff propagation.
app/api/wrapped/route.test.ts Updates wrapped API tests, but currently contains unresolved merge markers.
app/api/streak/tests/refresh.test.ts Adjusts mocks for streak refresh tests, but currently contains unresolved merge markers.
app/api/streak/route.ts Sets Retry-After header on 429 responses when a typed rate-limit error is thrown.
app/api/streak/route.test.ts Adds tests covering 429 + Retry-After behavior; updates mocking to keep RateLimitError.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/api/wrapped/route.test.ts Outdated
Comment on lines +264 to +268
<<<<<<< HEAD
expect(body).toContain('API RATE LIMIT');
=======
expect(body).toContain('rate limit'); //change to lowercase
>>>>>>> 8460ceee (fix(api): return fallback SVG and 429 on GitHub GraphQL rate-limit exhaustion)
Comment thread app/api/streak/tests/refresh.test.ts Outdated
Comment on lines +5 to +21
<<<<<<< HEAD
vi.mock('../../../../lib/github', () => ({
fetchGitHubContributions: vi.fn(),
getOrgDashboardData: vi.fn(),
getCircuitTelemetry: vi.fn().mockReturnValue({ isOpen: false, resetInMs: 0 }),
}));
=======
vi.mock('../../../../lib/github', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../../lib/github')>();
return {
...actual,
fetchGitHubContributions: vi.fn(),
getOrgDashboardData: vi.fn(),
getCircuitTelemetry: vi.fn(() => ({ isOpen: false, resetInMs: 0 })),
};
});
>>>>>>> 8460ceee (fix(api): return fallback SVG and 429 on GitHub GraphQL rate-limit exhaustion)
Comment thread lib/github.ts
Comment on lines +859 to +862
const resetAt = res.headers.get('x-ratelimit-reset') ?? undefined;
const retryAfterMs = resetAt
? Math.max(0, parseInt(resetAt, 10) * 1000 - Date.now())
: 60_000;
Comment thread lib/github.test.ts
Comment on lines 365 to +367
const promise = fetchGitHubContributions('octocat');
const assertion = expect(promise).rejects.toThrow('API Rate Limit Exceeded');
await vi.advanceTimersByTimeAsync(3500);
await assertion;
await vi.advanceTimersByTimeAsync(10000); // promise rejects HERE, with no handler yet
await expect(promise).rejects.toBeInstanceOf(RateLimitError);
Comment thread lib/github.test.ts
// ---------------------------------------------------------------------------
// fetchGitHubContributions — GraphQL RATE_LIMITED → typed RateLimitError
// ---------------------------------------------------------------------------
describe('fetchGitHubContributions — GraphQL RATE_LIMITED typed error', () => {
Comment on lines +9 to +24
vi.mock('../../../lib/github', () => {
class RateLimitError extends Error {
public retryAfterMs?: number;
constructor(message: string, retryAfterMs?: number) {
super(message);
this.name = 'RateLimitError';
this.retryAfterMs = retryAfterMs;
}
}
return {
fetchGitHubContributions: vi.fn(),
getOrgDashboardData: vi.fn(),
getCircuitTelemetry: vi.fn().mockReturnValue({ isOpen: false, resetInMs: 0 }),
RateLimitError,
};
});
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Hey @Payal-mak, the CI Pipeline is failing on this PR and it has been marked as status:blocked.

Please fix the issues before this can be reviewed. Here's how:

1. Run checks locally before pushing:

npm run format:check   # Check Prettier formatting
npm run lint           # Run ESLint
npm run typecheck      # TypeScript type check
npm run test           # Run unit tests (Vitest)
npm run build          # Verify production build passes

2. Auto-fix common issues:

npm run format         # Auto-fix formatting with Prettier
npm run lint -- --fix  # Auto-fix lint errors where possible

3. Check the full failure log here:
👉 View CI Run

Once you push a fix and the CI passes, the status:blocked label will be removed automatically. 💪

@github-actions github-actions Bot added the status:blocked This PR is blocked due to a failing CI check. label Jun 16, 2026
@Payal-mak Payal-mak force-pushed the fix/streak-rate-limit-fallback branch from ce0cc4d to 36b4a06 Compare June 16, 2026 02:35

@Aamod-Dev Aamod-Dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good rate-limit handling

@Aamod-Dev Aamod-Dev added level:intermediate Moderate complexity tasks quality:clean PR follows clean coding practices, proper formatting, documentation, and maintainability standards. type:bug Something isn't working as expected mentor:Aamod007 labels Jun 18, 2026

@Aamod-Dev Aamod-Dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Difficulty: intermediate – Returns fallback SVG and 429 status on GitHub GraphQL rate-limit exceeded.

Quality: clean – Graceful degradation.

Type: bug – Rate limit handling.

Good fallback!

@Aamod-Dev Aamod-Dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This PR cannot be approved in its current state due to blocking issues (status:blocked label, merge conflicts, needs-rebase label, and/or failing CI checks). Please resolve the blocking issues and re-request review.

Once unblocked, I'm happy to re-review! 💚

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:intermediate Moderate complexity tasks mentor:Aamod007 quality:clean PR follows clean coding practices, proper formatting, documentation, and maintainability standards. status:blocked This PR is blocked due to a failing CI check. type:bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: API returns unhandled 500 / blank response on GitHub GraphQL rate-limit exhaustion — no graceful fallback SVG

4 participants