Skip to content

Commit a424445

Browse files
committed
Upstash Support
1 parent dfbe248 commit a424445

File tree

4 files changed

+214
-46
lines changed

4 files changed

+214
-46
lines changed

README.md

+44-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# NextJS Rate Limiting Middleware
22

3-
Uses in-memory rate limiting for both session & IP. Doesn't require Redis, simple easy setup, and super basic protection from abuse.
3+
Uses in-memory rate limiting for both session & IP. Simple easy setup, and super basic protection from abuse. Now supports Upstash configuration for distributed rate limiting.
44

55
# Installation
66

@@ -10,15 +10,22 @@ npm install @daveyplate/next-rate-limit
1010

1111
# Usage
1212

13-
Default limits are 30 requests per session within 15 seconds, and 300 requests per IP within 15 seconds (10 users)
13+
Default limits are 20 requests per session within 10 seconds, and 100 requests per IP within 10 seconds.
1414

15-
```jsx
15+
```ts
1616
export function rateLimit({
1717
request,
1818
response,
19-
sessionLimit = 30,
20-
ipLimit = 300,
21-
windowMs = 15 * 1000
19+
sessionLimit = 20,
20+
ipLimit = 100,
21+
sessionWindow = 10,
22+
ipWindow = 10,
23+
upstash = {
24+
enabled: false,
25+
url: process.env.UPSTASH_REDIS_REST_URL,
26+
token: '',
27+
analytics: false
28+
}
2229
})
2330
```
2431

@@ -31,14 +38,41 @@ import { rateLimit } from '@daveyplate/next-rate-limit'
3138
export async function middleware(request: NextRequest) {
3239
const response = NextResponse.next()
3340
34-
const rateLimitResponse = await rateLimit({ request, response })
35-
if (rateLimitResponse) return rateLimitResponse
36-
37-
return response
41+
return await rateLimit({ request, response })
3842
}
3943
4044
// Apply middleware to all API routes
4145
export const config = {
4246
matcher: '/api/:path*'
4347
}
4448
```
49+
50+
# Upstash Configuration
51+
52+
To enable Upstash, you can configure it using environment variables or by passing the configuration directly.
53+
54+
## Environment Variables
55+
56+
Set the following environment variables in your `.env` file:
57+
58+
```
59+
UPSTASH_REDIS_REST_URL=<your_upstash_redis_rest_url>
60+
UPSTASH_REDIS_REST_TOKEN=<your_upstash_redis_rest_token>
61+
```
62+
63+
## Passing Configuration Directly
64+
65+
You can also pass the Upstash configuration directly when calling `rateLimit`:
66+
67+
```tsx
68+
const rateLimitResponse = await rateLimit({
69+
request,
70+
response,
71+
upstash: {
72+
enabled: true,
73+
url: '<your_upstash_redis_rest_url>',
74+
token: '<your_upstash_redis_rest_token>',
75+
analytics: true
76+
}
77+
})
78+
```

package-lock.json

+46-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@daveyplate/next-rate-limit",
33
"homepage": "https://github.com/Daveyplate/next-rate-limit",
4-
"version": "1.0.10",
4+
"version": "2.0.0",
55
"description": "NextJS Rate Limiting Middleware",
66
"module": "dist/index",
77
"types": "dist/index",
@@ -24,11 +24,12 @@
2424
"license": "ISC",
2525
"devDependencies": {
2626
"@types/uuid": "^10.0.0",
27-
"typescript": "^5.6.2",
28-
"next": "file:../daveyplate/node_modules/next"
27+
"next": "file:../daveyplate/node_modules/next",
28+
"typescript": "^5.6.2"
2929
},
3030
"peerDependencies": {
3131
"next": ">=14.0.0",
32-
"uuid": ">=10.0.0"
32+
"uuid": ">=10.0.0",
33+
"@upstash/ratelimit": ">=2.0.5"
3334
}
34-
}
35+
}

src/next-rate-limit.ts

+118-29
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,150 @@
1-
import { NextResponse, NextRequest } from 'next/server'
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { Duration, Ratelimit } from "@upstash/ratelimit"
3+
4+
import { Redis } from "@upstash/redis"
25
import { cookies } from 'next/headers'
36
import { v4 as uuidv4 } from 'uuid'
47

58
const rateLimitMap = new Map<string, number[]>()
69

10+
let redis: Redis
11+
let sessionRatelimit: Ratelimit
12+
let ipRatelimit: Ratelimit
13+
let currentConfig: any = {}
14+
15+
function initialize({ sessionLimit, sessionWindow, ipLimit, ipWindow, upstash }: Partial<RateLimitOptions>) {
16+
const { url, token, analytics } = upstash!
17+
18+
const config = {
19+
sessionLimit,
20+
sessionWindow,
21+
ipLimit,
22+
ipWindow,
23+
upstash
24+
}
25+
26+
if (!redis || JSON.stringify(config) !== JSON.stringify(currentConfig)) {
27+
currentConfig = config
28+
29+
redis = new Redis({
30+
url: url,
31+
token: token,
32+
})
33+
34+
sessionRatelimit = new Ratelimit({
35+
redis,
36+
limiter: Ratelimit.slidingWindow(sessionLimit!, `${sessionWindow} s` as Duration),
37+
analytics
38+
})
39+
40+
ipRatelimit = new Ratelimit({
41+
redis,
42+
limiter: Ratelimit.slidingWindow(ipLimit!, `${ipWindow} s` as Duration),
43+
analytics
44+
})
45+
}
46+
}
47+
48+
interface RateLimitOptions {
49+
request: NextRequest
50+
response: NextResponse
51+
sessionLimit?: number
52+
ipLimit?: number
53+
sessionWindow?: number
54+
ipWindow?: number
55+
upstash?: {
56+
enabled?: boolean
57+
url?: string
58+
token?: string
59+
analytics?: boolean
60+
}
61+
windowMs?: number
62+
}
763

864
/**
965
* This middleware is used to rate limit requests based
1066
* on IP address and session ID. Returns NextResponse with
1167
* 429 status code if the rate limit is exceeded.
12-
* @param {object} options - Rate limiting options.
13-
* @param {NextRequest} options.request - Incoming request object.
68+
* @param {RateLimitOptions} options - Rate limiting options.
69+
* @param {NextRequest} options.request - NextRequest object.
1470
* @param {NextResponse} options.response - NextResponse object.
15-
* @param {number} [options.sessionLimit=30] - Number of requests allowed per session in the window.
16-
* @param {number} [options.ipLimit=300] - Number of requests allowed per IP in the window.
17-
* @param {number} [options.windowMs=15000] - Time window in milliseconds.
71+
* @param {number} [options.sessionLimit=20] - Number of requests allowed per session in the window.
72+
* @param {number} [options.ipLimit=100] - Number of requests allowed per IP in the window.
73+
* @param {number} [options.sessionWindow=10] - Window in seconds for session rate limiting.
74+
* @param {number} [options.ipWindow=10] - Window in seconds for IP rate limiting.
75+
* @param {object} [options.upstash] - Upstash Redis configuration.
76+
* @param {boolean} [options.upstash.enabled] - Enable Upstash Redis rate limiting.
77+
* @param {string} [options.upstash.url] - Upstash Redis REST URL.
78+
* @param {string} [options.upstash.token] - Upstash Redis REST token.
79+
* @param {boolean} [options.upstash.analytics] - Enable analytics for rate limiting.
1880
*/
1981
export async function rateLimit({
2082
request,
2183
response,
22-
sessionLimit = 30,
23-
ipLimit = 300,
24-
windowMs = 15 * 1000
25-
}: { request: NextRequest; response: NextResponse; sessionLimit?: number; ipLimit?: number; windowMs?: number }) {
26-
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip")
84+
sessionLimit = 20,
85+
ipLimit = 100,
86+
sessionWindow = 10,
87+
ipWindow = 10,
88+
upstash = {},
89+
windowMs,
90+
}: RateLimitOptions) {
91+
const errorResponse = NextResponse.json({ message: "Too Many Requests" }, { status: 429 })
92+
93+
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || '127.0.0.1'
2794
let sessionId = (await cookies()).get('session_id')?.value
2895

2996
if (!sessionId) {
3097
sessionId = uuidv4()
3198
response.cookies.set("session_id", sessionId)
3299
}
33100

34-
// Check IP rate limit
35-
if (ip && handleRateLimiting(ip, ipLimit, windowMs)) {
36-
const response = { message: "Too Many Requests" }
37-
console.warn(`Rate limit exceeded for IP: ${ip}`)
38-
return new NextResponse(JSON.stringify(response), {
39-
status: 429,
40-
headers: {
41-
"Content-Type": "application/json"
101+
upstash.url = upstash.url || process.env.UPSTASH_REDIS_REST_URL
102+
upstash.token = upstash.token || process.env.UPSTASH_REDIS_REST_TOKEN
103+
104+
// Merge defaults with provided config
105+
if (upstash.enabled && upstash.url && upstash.token) {
106+
initialize({
107+
sessionLimit,
108+
sessionWindow,
109+
ipLimit,
110+
ipWindow,
111+
upstash: {
112+
url: upstash.url,
113+
token: upstash.token,
42114
}
43115
})
116+
117+
const { success, pending, limit, reset, remaining } =
118+
await sessionRatelimit.limit(sessionId)
119+
120+
if (!success) {
121+
console.warn(`Rate limit exceeded for session ID: ${sessionId}`)
122+
return errorResponse
123+
}
124+
125+
const { success: ipSuccess } = await ipRatelimit.limit(ip)
126+
127+
if (!ipSuccess) {
128+
console.warn(`Rate limit exceeded for IP: ${ip}`)
129+
return errorResponse
130+
}
131+
132+
return response
133+
}
134+
135+
// Check IP rate limit
136+
if (ip && handleRateLimiting(ip, ipLimit, windowMs || ipWindow * 1000)) {
137+
console.warn(`Rate limit exceeded for IP: ${ip}`)
138+
return errorResponse
44139
}
45140

46141
// Check session rate limit
47-
if (handleRateLimiting(sessionId, sessionLimit, windowMs)) {
48-
const response = { message: "Too Many Requests" }
142+
if (handleRateLimiting(sessionId, sessionLimit, windowMs || sessionWindow * 1000)) {
49143
console.warn(`Rate limit exceeded for session ID: ${sessionId}`)
50-
return new NextResponse(JSON.stringify(response), {
51-
status: 429,
52-
headers: {
53-
"Content-Type": "application/json"
54-
}
55-
})
144+
return errorResponse
56145
}
57146

58-
return null
147+
return response
59148
}
60149

61150
export function handleRateLimiting(key: string, limit: number, windowMs: number) {
@@ -81,4 +170,4 @@ export function handleRateLimiting(key: string, limit: number, windowMs: number)
81170
// Add the current timestamp to the list
82171
validTimestamps.push(currentTime)
83172
return false
84-
}
173+
}

0 commit comments

Comments
 (0)