fix: Rate limiter fail-open bug - wrap createClient in try/catch (qa-requested)

Fixes:
- Rate limiter now properly fails open when Supabase unavailable
- Moved createClient() calls inside try/catch blocks
- Prevents unhandled exceptions from reaching API handler
- Improved error logging for debugging

Impact:
- isRateLimited() - Wrapped createClient (line 22)
- getRemainingAttempts() - Wrapped createClient (line 93)
- getTimeToReset() - Wrapped createClient (line 128)

Context:
- QA Session 2 found API returning 500 errors
- Root cause: Database table doesn't exist (requires manual migration)
- This fix ensures rate limiter fails gracefully when DB unavailable
- With DB present, rate limiting will work as designed

Verified:
- TypeScript compiles without errors
- Code follows fail-open pattern
- Error logging improved

QA Fix Session: 2

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 13:09:40 +01:00
co-authored by Claude Sonnet 4.5
parent 1cc0264087
commit 281627beda
2 changed files with 276 additions and 18 deletions
+15 -18
View File
@@ -18,11 +18,10 @@ class SupabaseRateLimiter {
* @returns true if rate limited, false otherwise
*/
async isRateLimited(identifier: string): Promise<boolean> {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
try {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
// Get the current rate limit record for this identifier
const { data: existingRecord, error: fetchError } = await supabase
.from('rate_limits')
@@ -79,8 +78,8 @@ class SupabaseRateLimiter {
return false; // Not rate limited yet
} catch (error) {
console.error('Unexpected error in rate limiting:', error);
return false; // Fail open on unexpected errors
console.error('[RateLimiter] Unexpected error in isRateLimited - FAILING OPEN:', error);
return false; // Fail open on ALL errors
}
}
@@ -90,11 +89,10 @@ class SupabaseRateLimiter {
* @returns Number of remaining attempts (0 if rate limited)
*/
async getRemainingAttempts(identifier: string): Promise<number> {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
try {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
const { data: record, error } = await supabase
.from('rate_limits')
.select('count')
@@ -115,8 +113,8 @@ class SupabaseRateLimiter {
return Math.max(0, MAX_REQUESTS - record.count);
} catch (error) {
console.error('Unexpected error getting remaining attempts:', error);
return MAX_REQUESTS; // Fail open
console.error('[RateLimiter] Unexpected error in getRemainingAttempts - FAILING OPEN:', error);
return MAX_REQUESTS; // Fail open on ALL errors
}
}
@@ -126,11 +124,10 @@ class SupabaseRateLimiter {
* @returns Milliseconds until reset (0 if no active limit)
*/
async getTimeToReset(identifier: string): Promise<number> {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
try {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
const { data: record, error } = await supabase
.from('rate_limits')
.select('window_start')
@@ -153,8 +150,8 @@ class SupabaseRateLimiter {
const resetTime = windowStartTime + WINDOW_SIZE_MS;
return Math.max(0, resetTime - now);
} catch (error) {
console.error('Unexpected error getting time to reset:', error);
return 0;
console.error('[RateLimiter] Unexpected error in getTimeToReset - FAILING OPEN:', error);
return 0; // Fail open on ALL errors
}
}
}