Files
simple-mail-cleaner/backend/node_modules/@fastify/rate-limit/test/global-rate-limit.test.js
2026-01-22 22:22:48 +01:00

1452 lines
37 KiB
JavaScript

'use strict'
const { test, mock } = require('node:test')
const Fastify = require('fastify')
const rateLimit = require('../index')
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
test('Basic', async (t) => {
t.plan(15)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
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')
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')
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['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
clock.tick(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')
clock.reset()
})
test('With text timeWindow', async (t) => {
t.plan(15)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, timeWindow: '1s' })
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')
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')
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['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
clock.tick(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')
clock.reset()
})
test('With function timeWindow', async (t) => {
t.plan(15)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, timeWindow: (_, __) => 1000 })
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')
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')
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['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
clock.tick(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')
clock.reset()
})
test('When passing NaN to the timeWindow property then the timeWindow should be the default value - 60 seconds', async (t) => {
t.plan(5)
const clock = mock.timers
clock.enable(0)
const defaultTimeWindowInSeconds = '60'
const fastify = Fastify()
await fastify.register(rateLimit, { max: 1, timeWindow: NaN })
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(
res.headers['x-ratelimit-reset'],
defaultTimeWindowInSeconds
)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
// Wait for almost 60s to make sure the time limit is right
clock.tick(55 * 1000)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
// Wait for the seconds that left until the time limit reset
clock.tick(5 * 1000)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
clock.reset()
})
test('With ips allowList, allowed ips should not result in rate limiting', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
allowList: ['127.0.0.1']
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
})
test('With ips allowList, not allowed ips should result in rate limiting', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
allowList: ['1.1.1.1']
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
})
test('With ips whitelist', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
whitelist: ['127.0.0.1']
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
})
test('With function allowList', async (t) => {
t.plan(18)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
keyGenerator () {
return 42
},
allowList: function (req, key) {
t.assert.ok(req.headers)
t.assert.deepStrictEqual(key, 42)
return req.headers['x-my-header'] !== undefined
}
})
fastify.get('/', async () => 'hello!')
const allowListHeader = {
method: 'GET',
url: '/',
headers: {
'x-my-header': 'FOO BAR'
}
}
let res
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
})
test('With async/await function allowList', async (t) => {
t.plan(18)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
keyGenerator () {
return 42
},
allowList: async function (req, key) {
await sleep(1)
t.assert.ok(req.headers)
t.assert.deepStrictEqual(key, 42)
return req.headers['x-my-header'] !== undefined
}
})
fastify.get('/', async () => 'hello!')
const allowListHeader = {
method: 'GET',
url: '/',
headers: {
'x-my-header': 'FOO BAR'
}
}
let res
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject(allowListHeader)
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
})
test('With onExceeding option', async (t) => {
t.plan(5)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
onExceeding: function (req, key) {
if (req && key) t.assert.ok('onExceeding called')
}
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
})
test('With onExceeded option', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '2s',
onExceeded: function (req, key) {
if (req && key) t.assert.ok('onExceeded called')
}
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
})
test('With onBanReach option', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
ban: 1,
onBanReach: function (req) {
// onBanReach called
t.assert.ok(req)
}
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 403)
})
test('With keyGenerator', async (t) => {
t.plan(19)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000,
keyGenerator (req) {
t.assert.deepStrictEqual(req.headers['my-custom-header'], 'random-value')
return req.headers['my-custom-header']
}
})
fastify.get('/', async () => 'hello!')
const payload = {
method: 'GET',
url: '/',
headers: {
'my-custom-header': 'random-value'
}
}
let res
res = await fastify.inject(payload)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
res = await fastify.inject(payload)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject(payload)
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['retry-after'], '1')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
clock.tick(1100)
res = await fastify.inject(payload)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
clock.reset()
})
test('With async/await keyGenerator', async (t) => {
t.plan(16)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: 1000,
keyGenerator: async function (req) {
await sleep(1)
t.assert.deepStrictEqual(req.headers['my-custom-header'], 'random-value')
return req.headers['my-custom-header']
}
})
fastify.get('/', async () => 'hello!')
const payload = {
method: 'GET',
url: '/',
headers: {
'my-custom-header': 'random-value'
}
}
let res
res = await fastify.inject(payload)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
res = await fastify.inject(payload)
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'], '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: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
JSON.parse(res.payload)
)
await sleep(1100)
res = await fastify.inject(payload)
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
})
test('With CustomStore', async (t) => {
t.plan(15)
function CustomStore (options) {
this.options = options
this.current = 0
}
CustomStore.prototype.incr = function (key, cb) {
const timeWindow = this.options.timeWindow
this.current++
cb(null, { current: this.current, ttl: timeWindow - this.current * 1000 })
}
CustomStore.prototype.child = function (routeOptions) {
const store = new CustomStore(
Object.assign(this.options, routeOptions.config.rateLimit)
)
return store
}
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: 10000,
store: CustomStore
})
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'], '9')
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'], '8')
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'], '7')
t.assert.deepStrictEqual(res.headers['retry-after'], '7')
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 7 seconds'
},
JSON.parse(res.payload)
)
})
test('does not override the onRequest', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000
})
fastify.get(
'/',
{
onRequest: function (req, reply, next) {
t.assert.ok('onRequest called')
next()
}
},
async () => 'hello!'
)
const 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')
})
test('does not override the onRequest as an array', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: 1000
})
fastify.get(
'/',
{
onRequest: [
function (req, reply, next) {
t.assert.ok('onRequest called')
next()
}
]
},
async () => 'hello!'
)
const 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')
})
test('variable max', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: (req) => {
t.assert.ok(req)
return +req.headers['secret-max']
},
timeWindow: 1000
})
fastify.get('/', async () => 'hello')
const res = await fastify.inject({ url: '/', headers: { 'secret-max': 50 } })
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '50')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '49')
})
test('variable max contenders', async (t) => {
t.plan(7)
const fastify = Fastify()
await fastify.register(rateLimit, {
keyGenerator: (req) => req.headers['api-key'],
max: (req, key) => (key === 'pro' ? 3 : 2),
timeWindow: 10000
})
fastify.get('/', async () => 'hello')
const requestSequence = [
{ headers: { 'api-key': 'pro' }, status: 200, url: '/' },
{ headers: { 'api-key': 'pro' }, status: 200, url: '/' },
{ headers: { 'api-key': 'pro' }, status: 200, url: '/' },
{ headers: { 'api-key': 'pro' }, status: 429, url: '/' },
{ headers: { 'api-key': 'NOT' }, status: 200, url: '/' },
{ headers: { 'api-key': 'NOT' }, status: 200, url: '/' },
{ headers: { 'api-key': 'NOT' }, status: 429, url: '/' }
]
for (const item of requestSequence) {
const res = await fastify.inject({ url: item.url, headers: item.headers })
t.assert.deepStrictEqual(res.statusCode, item.status)
}
})
test('when passing NaN to max variable then it should use the default max - 1000', async (t) => {
t.plan(2002)
const defaultMax = 1000
const fastify = Fastify()
await fastify.register(rateLimit, {
max: NaN,
timeWindow: 10000
})
fastify.get('/', async () => 'hello')
for (let i = 0; i < defaultMax; i++) {
const res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000')
}
const res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000')
})
test('hide rate limit headers', async (t) => {
t.plan(14)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: 1000,
addHeaders: {
'x-ratelimit-limit': false,
'x-ratelimit-remaining': false,
'x-ratelimit-reset': false,
'retry-after': 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'], '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['content-type'],
'application/json; charset=utf-8'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-reset'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['retry-after'],
'the header must be missing'
)
clock.tick(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')
clock.reset()
})
test('hide rate limit headers on exceeding', async (t) => {
t.plan(14)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: 1000,
addHeadersOnExceeding: {
'x-ratelimit-limit': false,
'x-ratelimit-remaining': false,
'x-ratelimit-reset': false
}
})
fastify.get('/', async () => 'hello')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.notStrictEqual(
res.headers['x-ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-reset'],
'the header must be missing'
)
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'], '1')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.notStrictEqual(res.headers['x-ratelimit-reset'], undefined)
t.assert.deepStrictEqual(res.headers['retry-after'], '1')
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.notStrictEqual(
res.headers['x-ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-reset'],
'the header must be missing'
)
clock.reset()
})
test('hide rate limit headers at all times', async (t) => {
t.plan(14)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: 1000,
addHeaders: {
'x-ratelimit-limit': false,
'x-ratelimit-remaining': false,
'x-ratelimit-reset': false,
'retry-after': false
},
addHeadersOnExceeding: {
'x-ratelimit-limit': false,
'x-ratelimit-remaining': false,
'x-ratelimit-reset': false
}
})
fastify.get('/', async () => 'hello')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.notStrictEqual(
res.headers['x-ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-reset'],
'the header must be missing'
)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-reset'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['retry-after'],
'the header must be missing'
)
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.notStrictEqual(
res.headers['x-ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['x-ratelimit-reset'],
'the header must be missing'
)
clock.reset()
})
test('With ban', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
ban: 1
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 403)
})
test('stops fastify lifecycle after onRequest and before preValidation', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 1, timeWindow: 1000 })
let preValidationCallCount = 0
fastify.get(
'/',
{
preValidation: function (req, reply, next) {
t.assert.ok('preValidation called only once')
preValidationCallCount++
next()
}
},
async () => 'hello!'
)
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(preValidationCallCount, 1)
})
test('With enabled IETF Draft Spec', async (t) => {
t.plan(16)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 2,
timeWindow: '1s',
enableDraftSpec: true,
errorResponseBuilder: (req, context) => ({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second',
ttl: context.ttl
})
})
fastify.get('/', async () => 'hello!')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '1')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0')
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['ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0')
t.assert.deepStrictEqual(
res.headers['ratelimit-reset'],
res.headers['retry-after']
)
const { ttl, ...payload } = JSON.parse(res.payload)
t.assert.deepStrictEqual(
res.headers['retry-after'],
'' + Math.floor(ttl / 1000)
)
t.assert.deepStrictEqual(
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 second'
},
payload
)
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '1')
clock.reset()
})
test('hide IETF draft spec headers', async (t) => {
t.plan(14)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: 1000,
enableDraftSpec: true,
addHeaders: {
'ratelimit-limit': false,
'ratelimit-remaining': false,
'ratelimit-reset': false,
'retry-after': false
}
})
fastify.get('/', async () => 'hello')
let res
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['ratelimit-reset'], '1')
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(
res.headers['content-type'],
'application/json; charset=utf-8'
)
t.assert.notStrictEqual(
res.headers['ratelimit-limit'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['ratelimit-remaining'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['ratelimit-reset'],
'the header must be missing'
)
t.assert.notStrictEqual(
res.headers['retry-after'],
'the header must be missing'
)
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['ratelimit-reset'], '1')
clock.reset()
})
test('afterReset and Rate Limit remain the same when enableDraftSpec is enabled', async (t) => {
t.plan(13)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: '10s',
enableDraftSpec: true
})
fastify.get('/', async () => 'hello!')
const res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 200)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0')
clock.tick(500)
await retry('10')
clock.tick(1000)
await retry('9')
async function retry (timeLeft) {
const res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 429)
t.assert.deepStrictEqual(res.headers['ratelimit-limit'], '1')
t.assert.deepStrictEqual(res.headers['ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['ratelimit-reset'], timeLeft)
t.assert.deepStrictEqual(
res.headers['ratelimit-reset'],
res.headers['retry-after']
)
}
clock.reset()
})
test('Before async in "max"', async () => {
const fastify = Fastify()
await fastify.register(rateLimit, {
keyGenerator: (req) => req.headers['api-key'],
max: async (req, key) => requestSequence(key),
timeWindow: 10000
})
await fastify.get('/', async () => 'hello')
const requestSequence = async (key) => ((await key) === 'pro' ? 5 : 2)
})
test('exposeHeadRoutes', async (t) => {
const fastify = Fastify({
exposeHeadRoutes: true
})
await fastify.register(rateLimit, {
max: 10,
timeWindow: 1000
})
fastify.get('/', async () => 'hello!')
const res = await fastify.inject({
url: '/',
method: 'GET'
})
const resHead = await fastify.inject({
url: '/',
method: 'HEAD'
})
t.assert.deepStrictEqual(res.statusCode, 200, 'GET: Response status code')
t.assert.deepStrictEqual(
res.headers['x-ratelimit-limit'],
'10',
'GET: x-ratelimit-limit header (global rate limit)'
)
t.assert.deepStrictEqual(
res.headers['x-ratelimit-remaining'],
'9',
'GET: x-ratelimit-remaining header (global rate limit)'
)
t.assert.deepStrictEqual(
resHead.statusCode,
200,
'HEAD: Response status code'
)
t.assert.deepStrictEqual(
resHead.headers['x-ratelimit-limit'],
'10',
'HEAD: x-ratelimit-limit header (global rate limit)'
)
t.assert.deepStrictEqual(
resHead.headers['x-ratelimit-remaining'],
'8',
'HEAD: x-ratelimit-remaining header (global rate limit)'
)
})
test('When continue exceeding is on (Local)', async (t) => {
const fastify = Fastify()
await fastify.register(rateLimit, {
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')
})
test('on preHandler hook', async (t) => {
const fastify = Fastify()
await fastify.register(rateLimit, {
max: 1,
timeWindow: 10000,
hook: 'preHandler',
keyGenerator (req) {
return req.userId || req.ip
}
})
fastify.decorateRequest('userId', '')
fastify.addHook('preHandler', async (req) => {
const { userId } = req.query
if (userId) {
req.userId = userId
}
})
fastify.get('/', async () => 'fastify is awesome !')
const send = (userId) => {
let query
if (userId) {
query = { userId }
}
return fastify.inject({
url: '/',
method: 'GET',
query
})
}
const first = await send()
const second = await send()
const third = await send('123')
const fourth = await send('123')
const fifth = await send('234')
t.assert.deepStrictEqual(first.statusCode, 200)
t.assert.deepStrictEqual(second.statusCode, 429)
t.assert.deepStrictEqual(third.statusCode, 200)
t.assert.deepStrictEqual(fourth.statusCode, 429)
t.assert.deepStrictEqual(fifth.statusCode, 200)
})
test('ban directly', async (t) => {
t.plan(15)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, ban: 0, timeWindow: '1s' })
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')
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')
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'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
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)
)
clock.tick(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')
clock.reset()
})
test('wrong timewindow', async (t) => {
t.plan(15)
const clock = mock.timers
clock.enable(0)
const fastify = Fastify()
await fastify.register(rateLimit, { max: 2, ban: 0, timeWindow: '1s' })
fastify.get(
'/',
{
config: {
rateLimit: {
timeWindow: -5
}
}
},
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')
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')
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'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
t.assert.deepStrictEqual(res.headers['retry-after'], '60')
t.assert.deepStrictEqual(
{
statusCode: 403,
error: 'Forbidden',
message: 'Rate limit exceeded, retry in 1 minute'
},
JSON.parse(res.payload)
)
clock.tick(1100)
res = await fastify.inject('/')
t.assert.deepStrictEqual(res.statusCode, 403)
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
clock.reset()
})