feat: add rate limiter for API calls - #4
Conversation
There was a problem hiding this comment.
The PR adds a sliding-window rate limiter and a header parser in a single new file. There is one critical off-by-one correctness bug in tryAcquire that allows maxRequests + 1 requests per window, and a warning-level defect in parseRateLimit where parseInt on a missing value produces NaN that silently fails the truthiness guard. A few additional info-level concerns include bare any types, no NaN bounds-checking on parsed integers, negative windowMs accepted by reset, and no unit tests for the new code. Overall risk is medium — the core limiter logic is subtly incorrect, and the parser has fragile validation.
6 finding(s): 1 error, 1 warning, 4 info
[ERROR] correctness
File: src/utils/rate-limit.ts:20
Off-by-one: the condition this.timestamps.length <= this.config.maxRequests allows maxRequests+1 requests through. After filtering expired entries, if there are exactly maxRequests active timestamps, the check still passes and one more is pushed, exceeding the configured limit.
Suggestion: Change <= to <: if (this.timestamps.length < this.config.maxRequests) { ... }
[WARNING] error-handling
File: src/utils/rate-limit.ts:61
If a header part lacks = (e.g. malformed input), part.split("=") returns a single-element array, making val undefined. parseInt(undefined) yields NaN. The final if (maxRequests && windowMs) truthiness check rejects NaN (and also incorrectly rejects legitimate zero values), returning null without any indication of malformed input.
Suggestion: Validate the parse result explicitly: const parsed = parseInt(val, 10); if (isNaN(parsed)) continue; and use maxRequests !== null && windowMs !== null for the final guard (type maxRequests and windowMs as number | null instead of any).
[INFO] type-discipline
File: src/utils/rate-limit.ts:57
maxRequests and windowMs are typed as any, losing type safety. If the NaN path were later refactored, any would hide type errors.
Suggestion: Declare as let maxRequests: number | null = null; let windowMs: number | null = null;
[INFO] correctness
File: src/utils/rate-limit.ts:63
parseInt(val) without a radix parameter relies on implicit base-10 detection, which can produce surprising results for inputs with leading zeros or 0x prefixes.
Suggestion: Pass radix 10: parseInt(val, 10) and parseInt(val, 10) * 1000.
[INFO] correctness
File: src/utils/rate-limit.ts:50
reset(windowMs?) accepts any number including negative or zero values for windowMs, which would break the limiter's windowing logic (all timestamps would be considered expired or never expired).
Suggestion: Add a guard: if (windowMs !== undefined && windowMs > 0) { this.config.windowMs = windowMs; } or throw on invalid values.
[INFO] testing
File: src/utils/rate-limit.ts
No unit tests accompany the new rate limiter module. Given the off-by-one bug found, test coverage would have caught it.
Suggestion: Add tests covering: exact limit enforcement, window expiration, remainingRequests accuracy, waitForSlot behavior, and parseRateLimit for valid, invalid, and edge-case inputs.
Reviewed by pi-relay · View full run
| tryAcquire(): boolean { | ||
| const now = Date.now(); | ||
| this.timestamps = this.timestamps.filter((t) => now - t < this.config.windowMs); | ||
|
|
There was a problem hiding this comment.
[ERROR] correctness
Off-by-one: the condition this.timestamps.length <= this.config.maxRequests allows maxRequests+1 requests through. After filtering expired entries, if there are exactly maxRequests active timestamps, the check still passes and one more is pushed, exceeding the configured limit.
Suggestion: Change <= to <: if (this.timestamps.length < this.config.maxRequests) { ... }
| case "limit": | ||
| maxRequests = parseInt(val); | ||
| break; | ||
| case "window": |
There was a problem hiding this comment.
[WARNING] error-handling
If a header part lacks = (e.g. malformed input), part.split("=") returns a single-element array, making val undefined. parseInt(undefined) yields NaN. The final if (maxRequests && windowMs) truthiness check rejects NaN (and also incorrectly rejects legitimate zero values), returning null without any indication of malformed input.
Suggestion: Validate the parse result explicitly: const parsed = parseInt(val, 10); if (isNaN(parsed)) continue; and use maxRequests !== null && windowMs !== null for the final guard (type maxRequests and windowMs as number | null instead of any).
|
|
||
| for (const part of parts) { | ||
| const [key, val] = part.split("="); | ||
| switch (key.trim()) { |
There was a problem hiding this comment.
[INFO] type-discipline
maxRequests and windowMs are typed as any, losing type safety. If the NaN path were later refactored, any would hide type errors.
Suggestion: Declare as let maxRequests: number | null = null; let windowMs: number | null = null;
| break; | ||
| case "window": | ||
| windowMs = parseInt(val) * 1000; | ||
| break; |
There was a problem hiding this comment.
[INFO] correctness
parseInt(val) without a radix parameter relies on implicit base-10 detection, which can produce surprising results for inputs with leading zeros or 0x prefixes.
Suggestion: Pass radix 10: parseInt(val, 10) and parseInt(val, 10) * 1000.
| } | ||
| } | ||
|
|
||
| export function parseRateLimit(header: string): RateLimitConfig | null { |
There was a problem hiding this comment.
[INFO] correctness
reset(windowMs?) accepts any number including negative or zero values for windowMs, which would break the limiter's windowing logic (all timestamps would be considered expired or never expired).
Suggestion: Add a guard: if (windowMs !== undefined && windowMs > 0) { this.config.windowMs = windowMs; } or throw on invalid values.
AI Review: Fixes AppliedReviewed and found 6 issue(s). All addressed. Changes MadeFixed 2 findings in src/utils/rate-limit.ts:
Info findings (no tests, reset() validation) were not addressed as instructed. Verification passed. |
|
Test PR — verified updated reviewer prompt catches real defects and dismisses stale reviews after fixes. Closing. |
Adds a sliding-window rate limiter class and a header parser for rate limit responses.