Skip to content

Commit 5cc4a11

Browse files
committed
feat: add helper function to include rate limit headers and enhance tests for key generation errors
1 parent 7660c72 commit 5cc4a11

File tree

2 files changed

+63
-19
lines changed

2 files changed

+63
-19
lines changed

lib/middleware/rate-limit.js

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ function createRateLimit(options = {}) {
7474
skip,
7575
} = options
7676

77+
/**
78+
* Helper function to add rate limit headers to a response
79+
* @param {Response} response - Response object to add headers to
80+
* @param {number} totalHits - Current hit count
81+
* @param {Date} resetTime - When the rate limit resets
82+
* @returns {Response} Response with headers added
83+
*/
84+
const addRateLimitHeaders = (response, totalHits, resetTime) => {
85+
if (standardHeaders && response && response.headers) {
86+
response.headers.set('X-RateLimit-Limit', max.toString())
87+
response.headers.set(
88+
'X-RateLimit-Remaining',
89+
Math.max(0, max - totalHits).toString(),
90+
)
91+
response.headers.set(
92+
'X-RateLimit-Reset',
93+
Math.ceil(resetTime.getTime() / 1000).toString(),
94+
)
95+
response.headers.set('X-RateLimit-Used', totalHits.toString())
96+
}
97+
return response
98+
}
99+
77100
return async function rateLimitMiddleware(req, next) {
78101
// Allow test to inject a fresh store for isolation
79102
const activeStore = req && req.rateLimitStore ? req.rateLimitStore : store
@@ -92,22 +115,6 @@ function createRateLimit(options = {}) {
92115
const key = await keyGenerator(req)
93116
const {totalHits, resetTime} = await activeStore.increment(key, windowMs)
94117

95-
const createResponse = (response) => {
96-
if (standardHeaders && response && response.headers) {
97-
response.headers.set('X-RateLimit-Limit', max.toString())
98-
response.headers.set(
99-
'X-RateLimit-Remaining',
100-
Math.max(0, max - totalHits).toString(),
101-
)
102-
response.headers.set(
103-
'X-RateLimit-Reset',
104-
Math.ceil(resetTime.getTime() / 1000).toString(),
105-
)
106-
response.headers.set('X-RateLimit-Used', totalHits.toString())
107-
}
108-
return response
109-
}
110-
111118
if (totalHits > max) {
112119
let response
113120

@@ -121,7 +128,7 @@ function createRateLimit(options = {}) {
121128
}
122129
}
123130

124-
return createResponse(response)
131+
return addRateLimitHeaders(response, totalHits, resetTime)
125132
}
126133

127134
// Set rate limit context
@@ -143,7 +150,7 @@ function createRateLimit(options = {}) {
143150

144151
const response = await next()
145152
if (response instanceof Response) {
146-
return createResponse(response)
153+
return addRateLimitHeaders(response, totalHits, resetTime)
147154
}
148155
return response
149156
} catch (error) {
@@ -173,7 +180,7 @@ function createRateLimit(options = {}) {
173180

174181
const response = await next()
175182
if (response instanceof Response) {
176-
return response
183+
return addRateLimitHeaders(response, totalHits, resetTime)
177184
}
178185
return response
179186
} catch (e) {

test/unit/rate-limit.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,43 @@ describe('Rate Limit Middleware', () => {
432432
expect(req.rateLimit).toBeDefined() // Confirms we're in the fallback path
433433
expect(req.rateLimit.current).toBe(1) // Fallback incremented with 'unknown' key
434434
})
435+
436+
it('should include rate-limit headers in fallback path after key generation error', async () => {
437+
const faultyKeyGenerator = jest.fn(() => {
438+
throw new Error('Key generation error')
439+
})
440+
441+
// Create a Response object to be returned by next()
442+
const testResponse = new Response('test content', {
443+
status: 200,
444+
headers: {'X-Test': 'value'},
445+
})
446+
447+
const nextFunc = jest.fn(() => testResponse)
448+
449+
const middleware = rateLimit({
450+
windowMs: 60000,
451+
max: 5,
452+
keyGenerator: faultyKeyGenerator,
453+
standardHeaders: true, // Ensure headers are enabled
454+
})
455+
456+
const result = await middleware(req, nextFunc)
457+
458+
// Should return the response with added rate-limit headers
459+
expect(result).toBe(testResponse)
460+
expect(result.status).toBe(200)
461+
462+
// Verify rate-limit headers were added to the response
463+
expect(result.headers.get('X-RateLimit-Limit')).toBe('5')
464+
expect(result.headers.get('X-RateLimit-Remaining')).toBe('4') // 5 - 1 = 4
465+
expect(result.headers.get('X-RateLimit-Used')).toBe('1')
466+
expect(result.headers.get('X-RateLimit-Reset')).toBeTruthy()
467+
468+
// Verify we're in the fallback path
469+
expect(req.rateLimit).toBeDefined()
470+
expect(req.rateLimit.current).toBe(1) // Fallback incremented with 'unknown' key
471+
})
435472
})
436473

437474
describe('MemoryStore', () => {

0 commit comments

Comments
 (0)