0http-bun 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/middleware/README.md +115 -3
- package/lib/middleware/rate-limit.js +71 -107
- package/package.json +1 -1
package/lib/middleware/README.md
CHANGED
|
@@ -173,7 +173,7 @@ router.use(
|
|
|
173
173
|
},
|
|
174
174
|
}),
|
|
175
175
|
)
|
|
176
|
-
|
|
176
|
+
```
|
|
177
177
|
|
|
178
178
|
**TypeScript Usage:**
|
|
179
179
|
|
|
@@ -590,15 +590,127 @@ class CustomStore implements RateLimitStore {
|
|
|
590
590
|
}
|
|
591
591
|
```
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
#### Sliding Window Rate Limiter
|
|
594
|
+
|
|
595
|
+
For more precise rate limiting, use the sliding window implementation that **prevents burst traffic** at any point in time:
|
|
596
|
+
|
|
597
|
+
```javascript
|
|
598
|
+
const {createSlidingWindowRateLimit} = require('0http-bun/lib/middleware')
|
|
599
|
+
|
|
600
|
+
// Basic sliding window rate limiter
|
|
601
|
+
router.use(
|
|
602
|
+
createSlidingWindowRateLimit({
|
|
603
|
+
windowMs: 60 * 1000, // 1 minute sliding window
|
|
604
|
+
max: 10, // Max 10 requests per minute
|
|
605
|
+
keyGenerator: (req) => req.headers.get('x-forwarded-for') || 'default',
|
|
606
|
+
}),
|
|
607
|
+
)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**TypeScript Usage:**
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
import {createSlidingWindowRateLimit} from '0http-bun/lib/middleware'
|
|
614
|
+
import type {RateLimitOptions} from '0http-bun/lib/middleware'
|
|
615
|
+
|
|
616
|
+
const slidingOptions: RateLimitOptions = {
|
|
617
|
+
windowMs: 60 * 1000, // 1 minute
|
|
618
|
+
max: 10, // 10 requests max
|
|
619
|
+
keyGenerator: (req) => req.user?.id || req.headers.get('x-forwarded-for'),
|
|
620
|
+
handler: (req, hits, max, resetTime) => {
|
|
621
|
+
return Response.json(
|
|
622
|
+
{
|
|
623
|
+
error: 'Rate limit exceeded',
|
|
624
|
+
retryAfter: Math.ceil((resetTime.getTime() - Date.now()) / 1000),
|
|
625
|
+
limit: max,
|
|
626
|
+
used: hits,
|
|
627
|
+
},
|
|
628
|
+
{status: 429},
|
|
629
|
+
)
|
|
630
|
+
},
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
router.use(createSlidingWindowRateLimit(slidingOptions))
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**How Sliding Window Differs from Fixed Window:**
|
|
637
|
+
|
|
638
|
+
The sliding window approach provides **more accurate and fair rate limiting** by tracking individual request timestamps:
|
|
639
|
+
|
|
640
|
+
- **Fixed Window**: Divides time into discrete chunks (e.g., 09:00:00-09:00:59, 09:01:00-09:01:59)
|
|
641
|
+
- ⚠️ **Problem**: Allows burst traffic at window boundaries (20 requests in 2 seconds)
|
|
642
|
+
- **Sliding Window**: Uses a continuous, moving time window from current moment
|
|
643
|
+
- ✅ **Advantage**: Prevents bursts at any point in time (true rate limiting)
|
|
644
|
+
|
|
645
|
+
**Use Cases for Sliding Window:**
|
|
646
|
+
|
|
647
|
+
```javascript
|
|
648
|
+
// Financial API - Zero tolerance for payment bursts
|
|
649
|
+
router.use(
|
|
650
|
+
'/api/payments/*',
|
|
651
|
+
createSlidingWindowRateLimit({
|
|
652
|
+
windowMs: 60 * 1000, // 1 minute
|
|
653
|
+
max: 3, // Only 3 payment attempts per minute
|
|
654
|
+
keyGenerator: (req) => req.user.accountId,
|
|
655
|
+
}),
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
// User Registration - Prevent automated signups
|
|
659
|
+
router.use(
|
|
660
|
+
'/api/register',
|
|
661
|
+
createSlidingWindowRateLimit({
|
|
662
|
+
windowMs: 3600 * 1000, // 1 hour
|
|
663
|
+
max: 3, // 3 accounts per IP per hour
|
|
664
|
+
keyGenerator: (req) => req.headers.get('x-forwarded-for'),
|
|
665
|
+
}),
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
// File Upload - Prevent abuse
|
|
669
|
+
router.use(
|
|
670
|
+
'/api/upload',
|
|
671
|
+
createSlidingWindowRateLimit({
|
|
672
|
+
windowMs: 300 * 1000, // 5 minutes
|
|
673
|
+
max: 10, // 10 uploads per 5 minutes
|
|
674
|
+
keyGenerator: (req) => req.user.id,
|
|
675
|
+
}),
|
|
676
|
+
)
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Performance Considerations:**
|
|
680
|
+
|
|
681
|
+
- **Memory Usage**: Higher than fixed window (stores timestamp arrays)
|
|
682
|
+
- **Time Complexity**: O(n) per request where n = requests in window
|
|
683
|
+
- **Best For**: Critical APIs, financial transactions, user-facing features
|
|
684
|
+
- **Use Fixed Window For**: High-volume APIs where approximate limiting is acceptable
|
|
685
|
+
|
|
686
|
+
**Advanced Configuration:**
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
// Tiered rate limiting based on user level
|
|
690
|
+
const createTieredRateLimit = (req) => {
|
|
691
|
+
const userTier = req.user?.tier || 'free'
|
|
692
|
+
const configs = {
|
|
693
|
+
free: {windowMs: 60 * 1000, max: 10},
|
|
694
|
+
premium: {windowMs: 60 * 1000, max: 100},
|
|
695
|
+
enterprise: {windowMs: 60 * 1000, max: 1000},
|
|
696
|
+
}
|
|
697
|
+
return createSlidingWindowRateLimit(configs[userTier])
|
|
698
|
+
}
|
|
699
|
+
```
|
|
594
700
|
|
|
595
701
|
**Rate Limit Headers:**
|
|
596
702
|
|
|
703
|
+
Both rate limiters send the following headers when `standardHeaders: true`:
|
|
704
|
+
|
|
597
705
|
- `X-RateLimit-Limit` - Request limit
|
|
598
706
|
- `X-RateLimit-Remaining` - Remaining requests
|
|
599
707
|
- `X-RateLimit-Reset` - Reset time (Unix timestamp)
|
|
600
708
|
- `X-RateLimit-Used` - Used requests
|
|
601
709
|
|
|
710
|
+
**Error Handling:**
|
|
711
|
+
|
|
712
|
+
Rate limiting middleware allows errors to bubble up as proper HTTP 500 responses. If your `keyGenerator` function or custom `store.increment()` method throws an error, it will not be caught and masked - the error will propagate up the middleware chain for proper error handling.
|
|
713
|
+
|
|
602
714
|
## Creating Custom Middleware
|
|
603
715
|
|
|
604
716
|
### Basic Middleware
|
|
@@ -619,7 +731,7 @@ const customMiddleware = (req: ZeroRequest, next: StepFunction) => {
|
|
|
619
731
|
}
|
|
620
732
|
|
|
621
733
|
router.use(customMiddleware)
|
|
622
|
-
|
|
734
|
+
```
|
|
623
735
|
|
|
624
736
|
### Async Middleware
|
|
625
737
|
|
|
@@ -111,82 +111,50 @@ function createRateLimit(options = {}) {
|
|
|
111
111
|
return next()
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
114
|
+
// Generate key and record the request immediately - let errors bubble up
|
|
115
|
+
const key = await keyGenerator(req)
|
|
116
|
+
const {totalHits, resetTime} = await activeStore.increment(key, windowMs)
|
|
117
|
+
|
|
118
|
+
// Check if rate limit exceeded
|
|
119
|
+
if (totalHits > max) {
|
|
120
|
+
let response
|
|
121
|
+
|
|
122
|
+
// If a custom message is provided, use it as plain text
|
|
123
|
+
if (message) {
|
|
124
|
+
response = new Response(message, {status: 429})
|
|
125
|
+
} else {
|
|
126
|
+
response = await handler(req, totalHits, max, resetTime)
|
|
127
|
+
if (typeof response === 'string') {
|
|
128
|
+
response = new Response(response, {status: 429})
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
return addRateLimitHeaders(response, totalHits, resetTime)
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
req.ctx.rateLimit = {
|
|
137
|
-
limit: max,
|
|
138
|
-
used: totalHits,
|
|
139
|
-
remaining: Math.max(0, max - totalHits),
|
|
140
|
-
resetTime,
|
|
141
|
-
current: totalHits,
|
|
142
|
-
reset: resetTime,
|
|
143
|
-
}
|
|
144
|
-
req.rateLimit = {
|
|
145
|
-
limit: max,
|
|
146
|
-
remaining: Math.max(0, max - totalHits),
|
|
147
|
-
current: totalHits,
|
|
148
|
-
reset: resetTime,
|
|
149
|
-
}
|
|
132
|
+
return addRateLimitHeaders(response, totalHits, resetTime)
|
|
133
|
+
}
|
|
150
134
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
limit: max,
|
|
168
|
-
used: totalHits,
|
|
169
|
-
remaining: Math.max(0, max - totalHits),
|
|
170
|
-
resetTime,
|
|
171
|
-
current: totalHits,
|
|
172
|
-
reset: resetTime,
|
|
173
|
-
}
|
|
174
|
-
req.rateLimit = {
|
|
175
|
-
limit: max,
|
|
176
|
-
remaining: Math.max(0, max - totalHits),
|
|
177
|
-
current: totalHits,
|
|
178
|
-
reset: resetTime,
|
|
179
|
-
}
|
|
135
|
+
// Set rate limit context
|
|
136
|
+
req.ctx = req.ctx || {}
|
|
137
|
+
req.ctx.rateLimit = {
|
|
138
|
+
limit: max,
|
|
139
|
+
used: totalHits,
|
|
140
|
+
remaining: Math.max(0, max - totalHits),
|
|
141
|
+
resetTime,
|
|
142
|
+
current: totalHits,
|
|
143
|
+
reset: resetTime,
|
|
144
|
+
}
|
|
145
|
+
req.rateLimit = {
|
|
146
|
+
limit: max,
|
|
147
|
+
remaining: Math.max(0, max - totalHits),
|
|
148
|
+
current: totalHits,
|
|
149
|
+
reset: resetTime,
|
|
150
|
+
}
|
|
180
151
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return response
|
|
186
|
-
} catch (e) {
|
|
187
|
-
return next()
|
|
188
|
-
}
|
|
152
|
+
// Continue with request processing - let any errors bubble up
|
|
153
|
+
const response = await next()
|
|
154
|
+
if (response instanceof Response) {
|
|
155
|
+
return addRateLimitHeaders(response, totalHits, resetTime)
|
|
189
156
|
}
|
|
157
|
+
return response
|
|
190
158
|
}
|
|
191
159
|
}
|
|
192
160
|
|
|
@@ -253,47 +221,43 @@ function createSlidingWindowRateLimit(options = {}) {
|
|
|
253
221
|
const requests = new Map() // key -> array of timestamps
|
|
254
222
|
|
|
255
223
|
return async function slidingWindowRateLimitMiddleware(req, next) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Get existing requests for this key
|
|
261
|
-
let userRequests = requests.get(key) || []
|
|
224
|
+
// Generate key and record the request immediately - let errors bubble up
|
|
225
|
+
const key = await keyGenerator(req)
|
|
226
|
+
const now = Date.now()
|
|
262
227
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
228
|
+
// Get existing requests for this key
|
|
229
|
+
let userRequests = requests.get(key) || []
|
|
230
|
+
|
|
231
|
+
// Remove old requests outside the window
|
|
232
|
+
userRequests = userRequests.filter(
|
|
233
|
+
(timestamp) => now - timestamp < windowMs,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
// Check if limit exceeded
|
|
237
|
+
if (userRequests.length >= max) {
|
|
238
|
+
const response = await handler(
|
|
239
|
+
req,
|
|
240
|
+
userRequests.length,
|
|
241
|
+
max,
|
|
242
|
+
new Date(now + windowMs),
|
|
266
243
|
)
|
|
244
|
+
return response
|
|
245
|
+
}
|
|
267
246
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// Add current request
|
|
280
|
-
userRequests.push(now)
|
|
281
|
-
requests.set(key, userRequests)
|
|
282
|
-
|
|
283
|
-
// Add rate limit info to context
|
|
284
|
-
req.ctx = req.ctx || {}
|
|
285
|
-
req.ctx.rateLimit = {
|
|
286
|
-
limit: max,
|
|
287
|
-
used: userRequests.length,
|
|
288
|
-
remaining: max - userRequests.length,
|
|
289
|
-
resetTime: new Date(userRequests[0] + windowMs),
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return next()
|
|
293
|
-
} catch (error) {
|
|
294
|
-
console.error('Sliding window rate limiting error:', error)
|
|
295
|
-
return next()
|
|
247
|
+
// Add current request
|
|
248
|
+
userRequests.push(now)
|
|
249
|
+
requests.set(key, userRequests)
|
|
250
|
+
|
|
251
|
+
// Add rate limit info to context
|
|
252
|
+
req.ctx = req.ctx || {}
|
|
253
|
+
req.ctx.rateLimit = {
|
|
254
|
+
limit: max,
|
|
255
|
+
used: userRequests.length,
|
|
256
|
+
remaining: max - userRequests.length,
|
|
257
|
+
resetTime: new Date(userRequests[0] + windowMs),
|
|
296
258
|
}
|
|
259
|
+
|
|
260
|
+
return next()
|
|
297
261
|
}
|
|
298
262
|
}
|
|
299
263
|
|