-
Notifications
You must be signed in to change notification settings - Fork 8
Add rate limit to write endpoints #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MrImmortal09
merged 9 commits into
iiitl:main
from
Ewan-Dkhar:no-type/rate-limit-middleware
Apr 12, 2026
Merged
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
43644b6
(lib): add rate limit to write endpoints
Ewan-Dkhar ba12d90
fix linting errors
Ewan-Dkhar 2c740bb
Merge branch 'main' into no-type/rate-limit-middleware
Ewan-Dkhar 58dfcb4
Remove write methods from api/health/route.ts. Fix audit flooding issue.
Ewan-Dkhar a49be9f
fix lint issue
Ewan-Dkhar 28d33fb
resolve merge confilct
Ewan-Dkhar c557c24
Merge updated no-type/rate-liit-middleware
Ewan-Dkhar a903eb5
Implement better error handling, rate limit initialization and remove…
Ewan-Dkhar 0f3d9d9
Update error handling for rate limiting under redis failures
Ewan-Dkhar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // import { connectDB } from "./db"; | ||
|
|
||
| interface AuditLogParams { | ||
| severity: 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL'; | ||
| event: string; | ||
| target: string; | ||
| message: string; | ||
| } | ||
|
|
||
| export async function logToAudit(params: AuditLogParams) { | ||
| try { | ||
| // Establish/reuse the MongoDB connection | ||
| // await connectDB(); | ||
|
|
||
| // Perform your database operation | ||
| // Have to create model for AuditLog first | ||
| // await AuditLog.create({ | ||
| // severity: params.severity, | ||
| // event: params.event, | ||
| // target: params.target, | ||
| // message: params.message, | ||
| // timestamp: new Date(), | ||
| // }); | ||
|
|
||
| console.log("[AuditLog]", params.message); | ||
|
|
||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
| } catch (error) { | ||
| console.error("Failed to write to audit log:", error); | ||
| } | ||
| } | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { Ratelimit } from '@upstash/ratelimit'; | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
| import { Redis } from '@upstash/redis'; | ||
| import { logToAudit } from './audit-log'; | ||
|
|
||
| const redis = new Redis({ | ||
| url: process.env.UPSTASH_REDIS_REST_URL!, | ||
| token: process.env.UPSTASH_REDIS_REST_TOKEN!, | ||
| }); | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Define valid window formats expected by Upstash | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
| type Unit = 'ms' | 's' | 'm' | 'h' | 'd'; | ||
| type Duration = `${number} ${Unit}` | `${number}${Unit}`; | ||
|
|
||
| interface RateLimitConfig { | ||
| maxRequests: number; | ||
| window: Duration; | ||
| } | ||
|
|
||
| export async function limit(key: string, config: RateLimitConfig) { | ||
| // Initialize the sliding window limiter with the custom config | ||
| const ratelimit = new Ratelimit({ | ||
| redis, | ||
| limiter: Ratelimit.slidingWindow(config.maxRequests, config.window), | ||
| analytics: true, | ||
| }); | ||
|
|
||
| const result = await ratelimit.limit(key); | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Calculate standard Retry-After in seconds | ||
| const retryAfterSeconds = Math.ceil((result.reset - Date.now()) / 1000); | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Handle breach logging | ||
| if (!result.success) { | ||
| const breachKey = `breach_count:${key}`; | ||
|
|
||
| // Increment a separate counter for breaches, expiring after 1 hour | ||
| const breachCount = await redis.incr(breachKey); | ||
| if (breachCount === 1) { | ||
| await redis.expire(breachKey, 3600); | ||
| } | ||
|
|
||
| // If they hit the 429 limit 3+ times in an hour, escalate to AuditLog | ||
| if (breachCount >= 3) { | ||
| await logToAudit({ | ||
| severity: 'WARNING', | ||
| event: 'CONSISTENT_RATE_LIMIT_BREACH', | ||
| target: key, | ||
| message: `Key breached rate limits ${breachCount} times within the hour.`, | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return { | ||
| success: result.success, | ||
| limit: result.limit, | ||
| remaining: result.remaining, | ||
| reset: result.reset, | ||
| retryAfter: retryAfterSeconds > 0 ? retryAfterSeconds : 1, | ||
| }; | ||
| } | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { limit } from "./ratelimit"; | ||
|
|
||
| export function withRateLimit<T extends unknown[]>( | ||
| handler: (req: NextRequest, ...args: T) => Promise<Response> | Response | ||
| ) { | ||
| return async (req: NextRequest, ...args: T) => { | ||
| // Determine the user's IP or identifier to use as the rate limit key | ||
| const ip = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "127.0.0.1"; | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
|
|
||
| // Call your limit function (allow e.g. 5 requests per minute) | ||
| const { success, retryAfter } = await limit(ip, { | ||
| maxRequests: 5, | ||
| window: "1 m", | ||
| }); | ||
|
MrImmortal09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| if (!success) { | ||
| return NextResponse.json( | ||
| { error: "Too many requests. Please try again later." }, | ||
| { status: 429, headers: { "Retry-After": retryAfter.toString() } } | ||
| ); | ||
| } | ||
|
|
||
| // If rate limit checks pass, execute the actual API route logic | ||
| return handler(req, ...args); | ||
| }; | ||
| } | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.