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.
@@ -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
- try {
115
- const key = await keyGenerator(req)
116
- const {totalHits, resetTime} = await activeStore.increment(key, windowMs)
117
-
118
- if (totalHits > max) {
119
- let response
120
-
121
- // If a custom message is provided, use it as plain text
122
- if (message) {
123
- response = new Response(message, {status: 429})
124
- } else {
125
- response = await handler(req, totalHits, max, resetTime)
126
- if (typeof response === 'string') {
127
- response = new Response(response, {status: 429})
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
- // Set rate limit context
135
- req.ctx = req.ctx || {}
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
- const response = await next()
152
- if (response instanceof Response) {
153
- return addRateLimitHeaders(response, totalHits, resetTime)
154
- }
155
- return response
156
- } catch (error) {
157
- // If key generation fails, fallback to a default key
158
- try {
159
- const key = 'unknown'
160
- const {totalHits, resetTime} = await activeStore.increment(
161
- key,
162
- windowMs,
163
- )
164
-
165
- req.ctx = req.ctx || {}
166
- req.ctx.rateLimit = {
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
- const response = await next()
182
- if (response instanceof Response) {
183
- return addRateLimitHeaders(response, totalHits, resetTime)
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
- try {
257
- const key = await keyGenerator(req)
258
- const now = Date.now()
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
- // Remove old requests outside the window
264
- userRequests = userRequests.filter(
265
- (timestamp) => now - timestamp < windowMs,
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
- // Check if limit exceeded
269
- if (userRequests.length >= max) {
270
- const response = await handler(
271
- req,
272
- userRequests.length,
273
- max,
274
- new Date(now + windowMs),
275
- )
276
- return response
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0http-bun",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "0http for Bun",
5
5
  "main": "index.js",
6
6
  "scripts": {