Aktueller Stand

This commit is contained in:
2026-01-22 22:22:48 +01:00
parent 33e2bc61e2
commit fa5f3808bb
169 changed files with 58567 additions and 25460 deletions

View File

@@ -0,0 +1,224 @@
'use strict'
const { test, mock } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../index')
test('With global rate limit options', async t => {
t.plan(8)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
global: false,
max: 2,
timeWindow: 1000
})
const checkRateLimit = fastify.createRateLimit()
fastify.get('/', async (req, reply) => {
const limit = await checkRateLimit(req)
return limit
})
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 1,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: false,
isBanned: false
})
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 0,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: false,
isBanned: false
})
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 0,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: true,
isBanned: false
})
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 1,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: false,
isBanned: false
})
clock.reset()
})
test('With custom rate limit options', async t => {
t.plan(10)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
global: false,
max: 5,
timeWindow: 1000
})
const checkRateLimit = fastify.createRateLimit({
max: 2,
timeWindow: 1000,
ban: 1
})
fastify.get('/', async (req, reply) => {
const limit = await checkRateLimit(req)
return limit
})
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 1,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: false,
isBanned: false
})
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 0,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: false,
isBanned: false
})
// should be exceeded now
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 0,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: true,
isBanned: false
})
// should be banned now
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 0,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: true,
isBanned: true
})
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.json(), {
isAllowed: false,
key: '127.0.0.1',
max: 2,
timeWindow: 1000,
remaining: 1,
ttl: 1000,
ttlInSeconds: 1,
isExceeded: false,
isBanned: false
})
clock.reset()
})
test('With allow list', async t => {
t.plan(2)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
global: false,
max: 5,
timeWindow: 1000
})
const checkRateLimit = fastify.createRateLimit({
allowList: ['127.0.0.1'],
max: 2,
timeWindow: 1000
})
fastify.get('/', async (req, reply) => {
const limit = await checkRateLimit(req)
return limit
})
const res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
// expect a different return type because isAllowed is true
t.assert.deepStrictEqual(res.json(), {
isAllowed: true,
key: '127.0.0.1'
})
})

View File

@@ -0,0 +1,232 @@
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const Fastify = require('fastify')
const rateLimit = require('../index')
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
test('Exponential Backoff', async () => {
const fastify = Fastify()
// Register rate limit plugin with exponentialBackoff set to true in routeConfig
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
fastify.get(
'/expoential-backoff',
{
config: {
rateLimit: {
max: 2,
timeWindow: 500,
exponentialBackoff: true
}
}
},
async () => 'exponential backoff applied!'
)
// Test
const res = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
const res2 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
assert.deepStrictEqual(res2.statusCode, 200)
assert.deepStrictEqual(res2.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res2.headers['x-ratelimit-remaining'], '0')
const res3 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
assert.deepStrictEqual(res3.statusCode, 429)
assert.deepStrictEqual(res3.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res3.headers['x-ratelimit-remaining'], '0')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res3.payload)
)
const res4 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
assert.deepStrictEqual(res4.statusCode, 429)
assert.deepStrictEqual(res4.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res4.headers['x-ratelimit-remaining'], '0')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res4.payload)
)
// Wait for the window to reset
await sleep(1000)
const res5 = await fastify.inject({ url: '/expoential-backoff', method: 'GET' })
assert.deepStrictEqual(res5.statusCode, 200)
assert.deepStrictEqual(res5.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res5.headers['x-ratelimit-remaining'], '1')
})
test('Global Exponential Backoff', async () => {
const fastify = Fastify()
// Register rate limit plugin with exponentialBackoff set to true in routeConfig
await fastify.register(rateLimit, { max: 2, timeWindow: 500, exponentialBackoff: true })
fastify.get(
'/expoential-backoff-global',
{
config: {
rateLimit: {
max: 2,
timeWindow: 500
}
}
},
async () => 'exponential backoff applied!'
)
// Test
let res
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 2 seconds'
},
JSON.parse(res.payload)
)
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 4 seconds'
},
JSON.parse(res.payload)
)
})
test('MAx safe Exponential Backoff', async () => {
const fastify = Fastify()
// Register rate limit plugin with exponentialBackoff set to true in routeConfig
await fastify.register(rateLimit, { max: 2, timeWindow: 500, exponentialBackoff: true })
fastify.get(
'/expoential-backoff-global',
{
config: {
rateLimit: {
max: 2,
timeWindow: '285421 years'
}
}
},
async () => 'exponential backoff applied!'
)
// Test
let res
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 285421 years'
},
JSON.parse(res.payload)
)
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 285421 years'
},
JSON.parse(res.payload)
)
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 285421 years'
},
JSON.parse(res.payload)
)
res = await fastify.inject({ url: '/expoential-backoff-global', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 285421 years'
},
JSON.parse(res.payload)
)
})

View File

@@ -0,0 +1,120 @@
'use strict'
const { test, mock } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../../index')
test('issue #207 - when continueExceeding is true and the store is local then it should reset the rate-limit', async (t) => {
const clock = mock.timers
clock.enable()
const fastify = Fastify()
await fastify.register(rateLimit, {
global: false
})
fastify.get(
'/',
{
config: {
rateLimit: {
max: 1,
timeWindow: 5000,
continueExceeding: true
}
}
},
async () => {
return 'hello!'
}
)
const firstOkResponse = await fastify.inject({
url: '/',
method: 'GET'
})
const firstRateLimitResponse = await fastify.inject({
url: '/',
method: 'GET'
})
clock.tick(3000)
const secondRateLimitWithResettingTheRateLimitTimer = await fastify.inject({
url: '/',
method: 'GET'
})
// after this the total time passed is 6s which WITHOUT `continueExceeding` the next request should be OK
clock.tick(3000)
const thirdRateLimitWithResettingTheRateLimitTimer = await fastify.inject({
url: '/',
method: 'GET'
})
// After this the rate limiter should allow for new requests
clock.tick(5000)
const okResponseAfterRateLimitCompleted = await fastify.inject({
url: '/',
method: 'GET'
})
t.assert.deepStrictEqual(firstOkResponse.statusCode, 200)
t.assert.deepStrictEqual(firstRateLimitResponse.statusCode, 429)
t.assert.deepStrictEqual(
firstRateLimitResponse.headers['x-ratelimit-limit'],
'1'
)
t.assert.deepStrictEqual(
firstRateLimitResponse.headers['x-ratelimit-remaining'],
'0'
)
t.assert.deepStrictEqual(
firstRateLimitResponse.headers['x-ratelimit-reset'],
'5'
)
t.assert.deepStrictEqual(
secondRateLimitWithResettingTheRateLimitTimer.statusCode,
429
)
t.assert.deepStrictEqual(
secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'],
'1'
)
t.assert.deepStrictEqual(
secondRateLimitWithResettingTheRateLimitTimer.headers[
'x-ratelimit-remaining'
],
'0'
)
t.assert.deepStrictEqual(
secondRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'],
'5'
)
t.assert.deepStrictEqual(
thirdRateLimitWithResettingTheRateLimitTimer.statusCode,
429
)
t.assert.deepStrictEqual(
thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-limit'],
'1'
)
t.assert.deepStrictEqual(
thirdRateLimitWithResettingTheRateLimitTimer.headers[
'x-ratelimit-remaining'
],
'0'
)
t.assert.deepStrictEqual(
thirdRateLimitWithResettingTheRateLimitTimer.headers['x-ratelimit-reset'],
'5'
)
t.assert.deepStrictEqual(okResponseAfterRateLimitCompleted.statusCode, 200)
clock.reset(0)
})

View File

@@ -0,0 +1,87 @@
'use strict'
const { test, mock } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../../index')
test('issue #215 - when using local store, 2nd user should not be rate limited when the time window is passed for the 1st user', async (t) => {
t.plan(5)
const clock = mock.timers
clock.enable()
const fastify = Fastify()
await fastify.register(rateLimit, {
global: false
})
fastify.get(
'/',
{
config: {
rateLimit: {
max: 1,
timeWindow: 5000,
continueExceeding: false
}
}
},
async () => 'hello!'
)
const user1FirstRequest = await fastify.inject({
url: '/',
method: 'GET',
remoteAddress: '1.1.1.1'
})
// Waiting for the time to pass to make the 2nd user start in a different start point
clock.tick(3000)
const user2FirstRequest = await fastify.inject({
url: '/',
method: 'GET',
remoteAddress: '2.2.2.2'
})
const user2SecondRequestAndShouldBeRateLimited = await fastify.inject({
url: '/',
method: 'GET',
remoteAddress: '2.2.2.2'
})
// After this the total time passed for the 1st user is 6s and for the 2nd user only 3s
clock.tick(3000)
const user2ThirdRequestAndShouldStillBeRateLimited = await fastify.inject({
url: '/',
method: 'GET',
remoteAddress: '2.2.2.2'
})
// After this the total time passed for the 2nd user is 5.1s - he should not be rate limited
clock.tick(2100)
const user2OkResponseAfterRateLimitCompleted = await fastify.inject({
url: '/',
method: 'GET',
remoteAddress: '2.2.2.2'
})
t.assert.deepStrictEqual(user1FirstRequest.statusCode, 200)
t.assert.deepStrictEqual(user2FirstRequest.statusCode, 200)
t.assert.deepStrictEqual(
user2SecondRequestAndShouldBeRateLimited.statusCode,
429
)
t.assert.deepStrictEqual(
user2ThirdRequestAndShouldStillBeRateLimited.statusCode,
429
)
t.assert.deepStrictEqual(
user2OkResponseAfterRateLimitCompleted.statusCode,
200
)
clock.reset()
})

View File

@@ -0,0 +1,74 @@
'use strict'
const { test, mock } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../../index')
test("issue #284 - don't set the reply code automatically", async (t) => {
const clock = mock.timers
clock.enable()
const fastify = Fastify()
await fastify.register(rateLimit, {
global: false
})
fastify.setErrorHandler((err, _req, res) => {
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(err.statusCode, 429)
res.redirect('/')
})
fastify.get(
'/',
{
config: {
rateLimit: {
max: 1,
timeWindow: 5000,
continueExceeding: true
}
}
},
async () => {
return 'hello!'
}
)
const firstOkResponse = await fastify.inject({
url: '/',
method: 'GET'
})
const firstRateLimitResponse = await fastify.inject({
url: '/',
method: 'GET'
})
// After this the rate limiter should allow for new requests
clock.tick(5000)
const okResponseAfterRateLimitCompleted = await fastify.inject({
url: '/',
method: 'GET'
})
t.assert.deepStrictEqual(firstOkResponse.statusCode, 200)
t.assert.deepStrictEqual(firstRateLimitResponse.statusCode, 302)
t.assert.deepStrictEqual(
firstRateLimitResponse.headers['x-ratelimit-limit'],
'1'
)
t.assert.deepStrictEqual(
firstRateLimitResponse.headers['x-ratelimit-remaining'],
'0'
)
t.assert.deepStrictEqual(
firstRateLimitResponse.headers['x-ratelimit-reset'],
'5'
)
t.assert.deepStrictEqual(okResponseAfterRateLimitCompleted.statusCode, 200)
clock.reset(0)
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const Fastify = require('fastify')
const rateLimit = require('../index')
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
test('GroupId from routeConfig', async () => {
const fastify = Fastify()
// Register rate limit plugin with groupId in routeConfig
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
fastify.get(
'/routeWithGroupId',
{
config: {
rateLimit: {
max: 2,
timeWindow: 500,
groupId: 'group1' // groupId specified in routeConfig
}
}
},
async () => 'hello from route with groupId!'
)
// Test: Request should have the correct groupId in response
const res = await fastify.inject({ url: '/routeWithGroupId', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
})
test('GroupId from routeOptions', async () => {
const fastify = Fastify()
// Register rate limit plugin with groupId in routeOptions
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
fastify.get(
'/routeWithGroupIdFromOptions',
{
config: {
rateLimit: {
max: 2,
timeWindow: 500
// groupId not specified here
}
}
},
async () => 'hello from route with groupId from options!'
)
// Test: Request should have the correct groupId from routeOptions
const res = await fastify.inject({ url: '/routeWithGroupIdFromOptions', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
})
test('No groupId provided', async () => {
const fastify = Fastify()
// Register rate limit plugin without groupId
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
// Route without groupId
fastify.get(
'/noGroupId',
{
config: {
rateLimit: {
max: 2,
timeWindow: 500
}
}
},
async () => 'hello from no groupId route!'
)
let res
// Test without groupId
res = await fastify.inject({ url: '/noGroupId', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
res = await fastify.inject({ url: '/noGroupId', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject({ url: '/noGroupId', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
assert.deepStrictEqual(res.headers['retry-after'], '1')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
})
test('With multiple routes and custom groupId', async () => {
const fastify = Fastify()
// Register rate limit plugin
await fastify.register(rateLimit, { max: 2, timeWindow: 500 })
// Route 1 with groupId 'group1'
fastify.get(
'/route1',
{
config: {
rateLimit: {
max: 2,
timeWindow: 500,
groupId: 'group1'
}
}
},
async () => 'hello from route 1!'
)
// Route 2 with groupId 'group2'
fastify.get(
'/route2',
{
config: {
rateLimit: {
max: 2,
timeWindow: 1000,
groupId: 'group2'
}
}
},
async () => 'hello from route 2!'
)
let res
// Test Route 1
res = await fastify.inject({ url: '/route1', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
res = await fastify.inject({ url: '/route1', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject({ url: '/route1', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
assert.deepStrictEqual(res.headers['retry-after'], '1')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Test Route 2
res = await fastify.inject({ url: '/route2', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
res = await fastify.inject({ url: '/route2', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject({ url: '/route2', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 429)
assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
assert.deepStrictEqual(res.headers['retry-after'], '1')
assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Wait for the window to reset
await sleep(1000)
// After reset, Route 1 should succeed again
res = await fastify.inject({ url: '/route1', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
// Route 2 should also succeed after the reset
res = await fastify.inject({ url: '/route2', method: 'GET' })
assert.deepStrictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
})
test('Invalid groupId type', async () => {
const fastify = Fastify()
// Register rate limit plugin with a route having an invalid groupId
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
try {
fastify.get(
'/invalidGroupId',
{
config: {
rateLimit: {
max: 2,
timeWindow: 1000,
groupId: 123 // Invalid groupId type
}
}
},
async () => 'hello with invalid groupId!'
)
assert.fail('should throw')
console.log('HER')
} catch (err) {
assert.deepStrictEqual(err.message, 'groupId must be a string')
}
})

View File

@@ -0,0 +1,18 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../index')
test('Fastify close on local store', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
let counter = 1
fastify.addHook('onClose', (_instance, done) => {
counter++
done()
})
await fastify.close()
t.assert.deepStrictEqual(counter, 2)
})

View File

@@ -0,0 +1,116 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../index')
test('Set not found handler can be rate limited', async (t) => {
t.plan(18)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
t.assert.ok(fastify.rateLimit)
fastify.setNotFoundHandler(
{
preHandler: fastify.rateLimit()
},
function (_request, reply) {
t.assert.ok('Error handler has been called')
reply.status(404).send(new Error('Not found'))
}
)
let res
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 404)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 404)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual(JSON.parse(res.payload), {
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
})
})
test('Set not found handler can be rate limited with specific options', async (t) => {
t.plan(28)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
t.assert.ok(fastify.rateLimit)
fastify.setNotFoundHandler(
{
preHandler: fastify.rateLimit({
max: 4,
timeWindow: 2000
})
},
function (_request, reply) {
t.assert.ok('Error handler has been called')
reply.status(404).send(new Error('Not found'))
}
)
let res
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 404)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '3')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 404)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 404)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 404)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
res = await fastify.inject('/not-found')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '4')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['retry-after'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
t.assert.deepStrictEqual(JSON.parse(res.payload), {
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 2 seconds'
})
})

View File

@@ -0,0 +1,753 @@
'use strict'
const { test, describe } = require('node:test')
const Redis = require('ioredis')
const Fastify = require('fastify')
const rateLimit = require('../index')
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const REDIS_HOST = '127.0.0.1'
describe('Global rate limit', () => {
test('With redis store', async (t) => {
t.plan(21)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000,
redis
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.strictEqual(res.statusCode, 200)
t.assert.ok(res)
t.assert.strictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
await sleep(100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Not using fake timers here as we use an external Redis that would not be effected by this
await sleep(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
await redis.flushall()
await redis.quit()
})
test('With redis store (ban)', async (t) => {
t.plan(19)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 1,
ban: 1,
timeWindow: 1000,
redis
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 403)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 403,
error: 'Forbidden',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Not using fake timers here as we use an external Redis that would not be effected by this
await sleep(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
await redis.flushall()
await redis.quit()
})
test('Skip on redis error', async (t) => {
t.plan(9)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000,
redis,
skipOnError: true
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
await redis.flushall()
await redis.quit()
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
})
test('Throw on redis error', async (t) => {
t.plan(5)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000,
redis,
skipOnError: false
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
await redis.flushall()
await redis.quit()
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 500)
t.assert.deepStrictEqual(
res.body,
'{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}'
)
})
test('When continue exceeding is on (Redis)', async (t) => {
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
redis,
max: 1,
timeWindow: 5000,
continueExceeding: true
})
fastify.get('/', async () => 'hello!')
const first = await fastify.inject({
url: '/',
method: 'GET'
})
const second = await fastify.inject({
url: '/',
method: 'GET'
})
t.assert.deepStrictEqual(first.statusCode, 200)
t.assert.deepStrictEqual(second.statusCode, 429)
t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5')
await redis.flushall()
await redis.quit()
})
test('Redis with continueExceeding should not always return the timeWindow as ttl', async (t) => {
t.plan(19)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 2,
timeWindow: 3000,
continueExceeding: true,
redis
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3')
// After this sleep, we should not see `x-ratelimit-reset === 3` anymore
await sleep(1000)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '2')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3')
t.assert.deepStrictEqual(res.headers['retry-after'], '3')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 3 seconds'
},
JSON.parse(res.payload)
)
// Not using fake timers here as we use an external Redis that would not be effected by this
await sleep(1000)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '3')
await redis.flushall()
await redis.quit()
})
test('When use a custom nameSpace', async (t) => {
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000,
redis,
nameSpace: 'my-namespace:',
keyGenerator: (req) => req.headers['x-my-header']
})
fastify.get('/', async () => 'hello!')
const allowListHeader = {
method: 'GET',
url: '/',
headers: {
'x-my-header': 'custom name space'
}
}
let res
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Not using fake timers here as we use an external Redis that would not be effected by this
await sleep(1100)
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
await redis.flushall()
await redis.quit()
})
test('With redis store and exponential backoff', async (t) => {
t.plan(20)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000,
redis,
exponentialBackoff: true
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
// First attempt over the limit should have the normal timeWindow (1000ms)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Second attempt over the limit should have doubled timeWindow (2000ms)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['retry-after'], '2')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 2 seconds'
},
JSON.parse(res.payload)
)
await redis.flushall()
await redis.quit()
})
})
describe('Route rate limit', () => {
test('With redis store', async t => {
t.plan(19)
const fastify = Fastify()
const redis = new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
global: false,
redis
})
fastify.get('/', {
config: {
rateLimit: {
max: 2,
timeWindow: 1000
},
someOtherPlugin: {
someValue: 1
}
}
}, async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.strictEqual(res.statusCode, 429)
t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
t.assert.strictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
}, JSON.parse(res.payload))
// Not using fake timers here as we use an external Redis that would not be effected by this
await sleep(1100)
res = await fastify.inject('/')
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.strictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.strictEqual(res.headers['x-ratelimit-reset'], '1')
await redis.flushall()
await redis.quit()
})
test('Throw on redis error', async (t) => {
t.plan(6)
const fastify = Fastify()
const redis = new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
redis,
global: false
})
fastify.get(
'/',
{
config: {
rateLimit: {
max: 2,
timeWindow: 1000,
skipOnError: false
}
}
},
async () => 'hello!'
)
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
await redis.flushall()
await redis.quit()
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 500)
t.assert.deepStrictEqual(
res.body,
'{"statusCode":500,"error":"Internal Server Error","message":"Connection is closed."}'
)
})
test('Skip on redis error', async (t) => {
t.plan(9)
const fastify = Fastify()
const redis = new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
redis,
global: false
})
fastify.get(
'/',
{
config: {
rateLimit: {
max: 2,
timeWindow: 1000,
skipOnError: true
}
}
},
async () => 'hello!'
)
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
await redis.flushall()
await redis.quit()
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '2')
})
test('When continue exceeding is on (Redis)', async (t) => {
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
global: false,
redis
})
fastify.get(
'/',
{
config: {
rateLimit: {
timeWindow: 5000,
max: 1,
continueExceeding: true
}
}
},
async () => 'hello!'
)
const first = await fastify.inject({
url: '/',
method: 'GET'
})
const second = await fastify.inject({
url: '/',
method: 'GET'
})
t.assert.deepStrictEqual(first.statusCode, 200)
t.assert.deepStrictEqual(second.statusCode, 429)
t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5')
await redis.flushall()
await redis.quit()
})
test('When continue exceeding is off under route (Redis)', async (t) => {
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
global: false,
continueExceeding: true,
redis
})
fastify.get(
'/',
{
config: {
rateLimit: {
timeWindow: 5000,
max: 1,
continueExceeding: false
}
}
},
async () => 'hello!'
)
const first = await fastify.inject({
url: '/',
method: 'GET'
})
const second = await fastify.inject({
url: '/',
method: 'GET'
})
await sleep(2000)
const third = await fastify.inject({
url: '/',
method: 'GET'
})
t.assert.deepStrictEqual(first.statusCode, 200)
t.assert.deepStrictEqual(second.statusCode, 429)
t.assert.deepStrictEqual(second.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(second.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(second.headers['x-ratelimit-reset'], '5')
t.assert.deepStrictEqual(third.statusCode, 429)
t.assert.deepStrictEqual(third.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(third.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(third.headers['x-ratelimit-reset'], '3')
await redis.flushall()
await redis.quit()
})
test('Route-specific exponential backoff with redis store', async (t) => {
t.plan(17)
const fastify = Fastify()
const redis = await new Redis({ host: REDIS_HOST })
await fastify.register(rateLimit, {
global: false,
redis
})
fastify.get('/', {
config: {
rateLimit: {
max: 1,
timeWindow: 1000,
exponentialBackoff: true
}
}
}, async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '1')
// First attempt over the limit should have the normal timeWindow (1000ms)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
// Second attempt over the limit should have doubled timeWindow (2000ms)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['retry-after'], '2')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 2 seconds'
},
JSON.parse(res.payload)
)
// Third attempt over the limit should have quadrupled timeWindow (4000ms)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['retry-after'], '4')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 4 seconds'
},
JSON.parse(res.payload)
)
await redis.flushall()
await redis.quit()
})
})

File diff suppressed because it is too large Load Diff