-
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 8 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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { Ratelimit } from '@upstash/ratelimit'; | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
| import { Redis } from '@upstash/redis'; | ||
|
|
||
| const redisClient = (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) | ||
| ? new Redis({ | ||
| url: process.env.UPSTASH_REDIS_REST_URL, | ||
| token: process.env.UPSTASH_REDIS_REST_TOKEN, | ||
| }) | ||
| : null; | ||
|
|
||
| type Unit = 'ms' | 's' | 'm' | 'h' | 'd'; | ||
| type Duration = `${number} ${Unit}` | `${number}${Unit}`; | ||
|
|
||
| interface RateLimitConfig { | ||
| maxRequests: number; | ||
| window: Duration; | ||
| } | ||
|
|
||
| const ratelimitCache = new Map<string, Ratelimit>(); | ||
|
|
||
| export async function limit(key: string, config: RateLimitConfig) { | ||
| if (!redisClient) { | ||
| console.warn("Rate limiting bypassed: UPSTASH_REDIS credentials are missing."); | ||
| return { success: true, limit: config.maxRequests, remaining: config.maxRequests, reset: 0, retryAfter: 0 }; | ||
| } | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
|
|
||
| // Retrieve existing rate limiter from cache, or create and cache a new one for this config | ||
| const cacheKey = `${config.maxRequests}-${config.window}`; | ||
| let ratelimit = ratelimitCache.get(cacheKey); | ||
|
|
||
| if (!ratelimit) { | ||
| ratelimit = new Ratelimit({ | ||
| redis: redisClient, | ||
| limiter: Ratelimit.slidingWindow(config.maxRequests, config.window), | ||
| analytics: true, | ||
| }); | ||
| ratelimitCache.set(cacheKey, ratelimit); | ||
| } | ||
|
|
||
| try { | ||
| const result = await ratelimit.limit(key); | ||
|
|
||
| // Calculate standard Retry-After in seconds | ||
| const retryAfterSeconds = Math.max(1, Math.ceil((result.reset - Date.now()) / 1000)); | ||
|
|
||
| if (!result.success) { | ||
| const breachKey = `breach_count:${key}`; | ||
|
|
||
| const pipeline = redisClient.pipeline(); | ||
| pipeline.incr(breachKey); | ||
| pipeline.expire(breachKey, 3600); | ||
|
|
||
| const results = await pipeline.exec(); | ||
| const breachCount = results[0] as number; | ||
|
|
||
| if (breachCount === 3) { | ||
| console.log({ | ||
| 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.
|
||
| } | ||
|
|
||
| return { | ||
| success: result.success, | ||
| limit: result.limit, | ||
| remaining: result.remaining, | ||
| reset: result.reset, | ||
| retryAfter: retryAfterSeconds, | ||
| }; | ||
| } catch (error) { | ||
| console.error("Rate limiting error:", error); | ||
| return { | ||
| success: true, | ||
| limit: config.maxRequests, | ||
| remaining: config.maxRequests, | ||
| reset: 0, | ||
| retryAfter: 0, | ||
| }; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
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,45 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { limit } from "./ratelimit"; | ||
|
|
||
| /** | ||
| * Wraps a Next.js API route handler to apply rate limiting. | ||
| * | ||
| * @example | ||
| * export const POST = withRateLimit(async (req: NextRequest) => { | ||
| * // Create logic | ||
| * return Response.json({ success: true }); | ||
| * }); | ||
| * | ||
| * export const PUT = withRateLimit(async (req: NextRequest) => { | ||
| * // Update logic | ||
| * return Response.json({ success: true }); | ||
| * }); | ||
| * | ||
| * export const DELETE = withRateLimit(async (req: NextRequest) => { | ||
| * // Delete logic | ||
| * return Response.json({ success: true }); | ||
| * }); | ||
| */ | ||
| export function withRateLimit<T extends unknown[]>( | ||
| handler: (req: NextRequest, ...args: T) => Promise<Response> | Response | ||
| ) { | ||
| return async (req: NextRequest, ...args: T) => { | ||
| const ip = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "127.0.0.1"; | ||
|
MrImmortal09 marked this conversation as resolved.
|
||
|
|
||
| const identifier = `${req.nextUrl.pathname}:${ip}`; | ||
|
|
||
| const { success, retryAfter } = await limit(identifier, { | ||
| maxRequests: 5, | ||
| window: "1 m", | ||
| }); | ||
|
|
||
| if (!success) { | ||
| return NextResponse.json( | ||
| { error: "Too many requests. Please try again later." }, | ||
| { status: 429, headers: { "Retry-After": retryAfter.toString() } } | ||
| ); | ||
| } | ||
|
|
||
| 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.