openreader/src/lib/server/rate-limit/problem-response.ts
Richard R d7e6a7b7ba fix(api,rate-limit): centralize daily quota exceeded response and update imports
Move daily quota exceeded problem response logic to a dedicated utility in `lib/server/rate-limit/problem-response`. Refactor API routes to use the new builder and remove duplicated formatting code. Update all relevant imports for consistency. This improves maintainability and ensures uniform error responses across TTS endpoints.
2026-05-12 18:24:39 -06:00

45 lines
1.6 KiB
TypeScript

import { NextResponse } from 'next/server';
import { RATE_LIMITS, type RateLimitResult } from '@/lib/server/rate-limit/rate-limiter';
function formatLimitForHint(limit: number): string {
if (!Number.isFinite(limit) || limit <= 0) return String(limit);
if (limit >= 1_000_000) {
const m = limit / 1_000_000;
return `${m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)}M`;
}
if (limit >= 1_000) return `${Math.round(limit / 1_000)}K`;
return String(limit);
}
export function buildDailyQuotaExceededResponse(input: {
rateLimitResult: RateLimitResult;
isAnonymousUser: boolean;
pathname: string;
}): NextResponse {
const { rateLimitResult, isAnonymousUser, pathname } = input;
const resetTimeMs = rateLimitResult.resetTimeMs;
const retryAfterSeconds = Math.max(0, Math.ceil((resetTimeMs - Date.now()) / 1000));
return new NextResponse(JSON.stringify({
type: 'https://openreader.app/problems/daily-quota-exceeded',
title: 'Daily quota exceeded',
status: 429,
detail: 'Daily character limit exceeded',
code: 'USER_DAILY_QUOTA_EXCEEDED',
currentCount: rateLimitResult.currentCount,
limit: rateLimitResult.limit,
remainingChars: rateLimitResult.remainingChars,
resetTimeMs,
userType: isAnonymousUser ? 'anonymous' : 'authenticated',
upgradeHint: isAnonymousUser
? `Sign up to increase your limit from ${formatLimitForHint(RATE_LIMITS.ANONYMOUS)} to ${formatLimitForHint(RATE_LIMITS.AUTHENTICATED)} characters per day`
: undefined,
instance: pathname,
}), {
status: 429,
headers: {
'Content-Type': 'application/problem+json',
'Retry-After': String(retryAfterSeconds),
},
});
}