Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/streak/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1653,7 +1653,7 @@ describe('GET /api/streak', () => {
expect(fetchGitHubContributions).not.toHaveBeenCalled();

const body = await response.text();
expect(body).toContain('strictly accepts a maximum of 2 usernames');
expect(body).toContain('a maximum of 2 usernames');
});
});

Expand Down
42 changes: 29 additions & 13 deletions app/api/streak/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
calculateStreak,
calculateMonthlyStats,
aggregateCalendars,
convertLocalToUtc,

Check warning on line 15 in app/api/streak/route.ts

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

'convertLocalToUtc' is defined but never used
chunkDaysIntoWeeks,
normalizeCalendarToTimezone,
isLeapYear,

Check warning on line 18 in app/api/streak/route.ts

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

'isLeapYear' is defined but never used
daysInYear,
} from '@/lib/calculate';
import {
Expand Down Expand Up @@ -712,7 +712,23 @@
if (message.includes('schema') || message.includes('Schema')) {
return 'Invalid request parameters';
}
return message;
// Preserve user-facing validation messages — these are intentional,
// safe error strings thrown by route-level validation and do not
// expose internal implementation details.
const lower = message.toLowerCase();
if (lower.includes('strictly for organizations')) {
return 'This endpoint is strictly for organizations.';
}
if (lower.includes('strictly accepts a maximum of 2')) {
return 'The streak comparison generator strictly accepts a maximum of 2 usernames.';
}
if (lower.includes('quota is low')) {
return 'API rate limit quota is low. Please try again later.';
}
// Issue #7263: Return a generic message for all other errors to
// prevent leaking internal implementation details (auth state, cache
// servers, token rotation info, etc.) to the client.
return 'Something went wrong. Please try again later.';
}

function buildErrorResponse(error: unknown, parseResult: ParseResult): NextResponse {
Expand All @@ -721,14 +737,14 @@

if (parseResult.success && parseResult.data.format === 'json') {
const isNotFound =
message.toLowerCase().includes('not found') ||
message.toLowerCase().includes('could not resolve');
const isRateLimit = message.toLowerCase().includes('rate limit');
rawMessage.toLowerCase().includes('not found') ||
rawMessage.toLowerCase().includes('could not resolve');
const isRateLimit = rawMessage.toLowerCase().includes('rate limit');
const isValidationError =
(error instanceof Error && error.name === 'ValidationError') ||
message.toLowerCase().includes('invalid') ||
message.toLowerCase().includes('validation') ||
message.toLowerCase().includes('strictly for organizations');
rawMessage.toLowerCase().includes('invalid') ||
rawMessage.toLowerCase().includes('validation') ||
rawMessage.toLowerCase().includes('strictly for organizations');

const status = isRateLimit ? 429 : isNotFound ? 404 : isValidationError ? 400 : 500;
const jsonErrorHeaders: Record<string, string> = {
Expand All @@ -748,16 +764,16 @@
}

const isNotFound =
message.toLowerCase().includes('not found') ||
message.toLowerCase().includes('could not resolve');
const isRateLimit = message.toLowerCase().includes('rate limit');
rawMessage.toLowerCase().includes('not found') ||
rawMessage.toLowerCase().includes('could not resolve');
const isRateLimit = rawMessage.toLowerCase().includes('rate limit');

// 2. Safely detect if the error was a validation/client error
const isValidationError =
(error instanceof Error && error.name === 'ValidationError') ||
message.toLowerCase().includes('invalid') ||
message.toLowerCase().includes('validation') ||
message.toLowerCase().includes('strictly for organizations');
rawMessage.toLowerCase().includes('invalid') ||
rawMessage.toLowerCase().includes('validation') ||
rawMessage.toLowerCase().includes('strictly for organizations');

const errBg = `#${sanitizeHexColor(parseResult.success ? parseResult.data.bg : undefined, '0d1117')}`;
const errAccentRaw =
Expand Down
Loading