From 82b4a4e558bbde6a6b07bdfb41bf8b15c0001292 Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Mon, 8 Dec 2025 00:00:19 +0000 Subject: [PATCH] fix: standardize rate limit headers and remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use rateLimitHeaders() spread on /api/posts success response to include X-RateLimit-Limit, X-RateLimit-Reset, Retry-After matching /api/search and /api/search/suggestions behavior - Remove legacy checkRateLimit, RateLimitResult, rateLimitStore, and cleanup interval from src/lib/search.ts (dead code after migration to central searchLimiter) - Update tests to use searchLimiter from @/lib/security instead of the removed checkRateLimit All integration tests pass (20 passed, 12 skipped). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app/(frontend)/api/posts/route.ts | 2 +- src/lib/search.ts | 48 --------------------------- tests/int/search.int.spec.ts | 14 ++++---- 3 files changed, 8 insertions(+), 56 deletions(-) diff --git a/src/app/(frontend)/api/posts/route.ts b/src/app/(frontend)/api/posts/route.ts index 7ef3c6f..ebb0e48 100644 --- a/src/app/(frontend)/api/posts/route.ts +++ b/src/app/(frontend)/api/posts/route.ts @@ -134,7 +134,7 @@ export async function GET(request: NextRequest) { return NextResponse.json(response, { headers: { - 'X-RateLimit-Remaining': String(rateLimit.remaining), + ...rateLimitHeaders(rateLimit, POSTS_RATE_LIMIT), 'Cache-Control': 'public, max-age=60, s-maxage=60', }, }) diff --git a/src/lib/search.ts b/src/lib/search.ts index e5e7e7a..640212d 100644 --- a/src/lib/search.ts +++ b/src/lib/search.ts @@ -79,12 +79,6 @@ export interface PaginatedPosts { hasPrevPage: boolean } -export interface RateLimitResult { - allowed: boolean - remaining: number - retryAfter?: number -} - // ============================================================================ // TTL Cache Implementation // ============================================================================ @@ -154,48 +148,6 @@ class TTLCache { export const searchCache = new TTLCache(60) export const suggestionCache = new TTLCache(60) -// ============================================================================ -// Rate Limiting -// ============================================================================ - -interface RateLimitEntry { - count: number - windowStart: number -} - -const rateLimitStore = new Map() -const RATE_LIMIT_WINDOW_MS = 60000 // 1 minute -const RATE_LIMIT_MAX_REQUESTS = 30 // 30 requests per minute - -// Cleanup rate limit entries every 5 minutes -setInterval(() => { - const now = Date.now() - for (const [key, entry] of rateLimitStore.entries()) { - if (now - entry.windowStart > RATE_LIMIT_WINDOW_MS * 2) { - rateLimitStore.delete(key) - } - } -}, 300000) - -export function checkRateLimit(ip: string): RateLimitResult { - const now = Date.now() - const entry = rateLimitStore.get(ip) - - if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) { - // New window - rateLimitStore.set(ip, { count: 1, windowStart: now }) - return { allowed: true, remaining: RATE_LIMIT_MAX_REQUESTS - 1 } - } - - if (entry.count >= RATE_LIMIT_MAX_REQUESTS) { - const retryAfter = Math.ceil((entry.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000) - return { allowed: false, remaining: 0, retryAfter } - } - - entry.count++ - return { allowed: true, remaining: RATE_LIMIT_MAX_REQUESTS - entry.count } -} - // ============================================================================ // Helper Functions // ============================================================================ diff --git a/tests/int/search.int.spec.ts b/tests/int/search.int.spec.ts index 9461626..3519e98 100644 --- a/tests/int/search.int.spec.ts +++ b/tests/int/search.int.spec.ts @@ -5,11 +5,11 @@ import { searchPosts, getSearchSuggestions, getPostsByCategory, - checkRateLimit, searchCache, suggestionCache, extractTextFromLexical, } from '@/lib/search' +import { searchLimiter } from '@/lib/security' let payload: Payload @@ -78,24 +78,24 @@ describe('Search Library', () => { }) }) - describe('checkRateLimit', () => { - it('allows requests within limit', () => { + describe('searchLimiter (central rate limiter)', () => { + it('allows requests within limit', async () => { const testIp = `test-${Date.now()}-1` - const result = checkRateLimit(testIp) + const result = await searchLimiter.check(testIp) expect(result.allowed).toBe(true) expect(result.remaining).toBe(29) // 30 - 1 }) - it('blocks requests exceeding limit', () => { + it('blocks requests exceeding limit', async () => { const testIp = `test-${Date.now()}-2` // Use up all requests for (let i = 0; i < 30; i++) { - checkRateLimit(testIp) + await searchLimiter.check(testIp) } - const result = checkRateLimit(testIp) + const result = await searchLimiter.check(testIp) expect(result.allowed).toBe(false) expect(result.remaining).toBe(0) expect(result.retryAfter).toBeGreaterThan(0)