1770 lines
44 KiB
JavaScript
1770 lines
44 KiB
JavaScript
'use strict'
|
|
|
|
const { test, mock } = require('node:test')
|
|
const Fastify = require('fastify')
|
|
const rateLimit = require('../index')
|
|
|
|
const defaultRouteConfig = {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: 1000
|
|
},
|
|
someOtherPlugin: {
|
|
someValue: 1
|
|
}
|
|
}
|
|
|
|
test('Basic', async (t) => {
|
|
t.plan(20)
|
|
|
|
const clock = mock.timers
|
|
clock.enable(0)
|
|
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: defaultRouteConfig
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
fastify.setErrorHandler(function (error, _request, reply) {
|
|
// t.assert.ok('Error handler has been called')
|
|
t.assert.deepStrictEqual(error.statusCode, 429)
|
|
reply.code(429)
|
|
error.message += ' from error handler'
|
|
reply.send(error)
|
|
})
|
|
|
|
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')
|
|
|
|
// Ticking time to simulate time been passed, passing `shouldAdvanceTime: true` won't help as between the 2 requests
|
|
// the event loop not reached the timer stage and is not able to run the `setInterval` that sinonjs/fake-timers use internally to update the time
|
|
clock.tick(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')
|
|
|
|
clock.tick(500)
|
|
|
|
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 from error handler'
|
|
},
|
|
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')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '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, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: '1s'
|
|
},
|
|
someOtherPlugin: {
|
|
someValue: 1
|
|
}
|
|
}
|
|
},
|
|
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(JSON.parse(res.payload), {
|
|
statusCode: 429,
|
|
error: 'Too Many Requests',
|
|
message: 'Rate limit exceeded, retry in 1 second'
|
|
})
|
|
|
|
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, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: (_, __) => 1000
|
|
},
|
|
someOtherPlugin: {
|
|
someValue: 1
|
|
}
|
|
}
|
|
},
|
|
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(JSON.parse(res.payload), {
|
|
statusCode: 429,
|
|
error: 'Too Many Requests',
|
|
message: 'Rate limit exceeded, retry in 1 second'
|
|
})
|
|
|
|
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 ips allowList', async (t) => {
|
|
t.plan(3)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
allowList: ['127.0.0.1']
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: defaultRouteConfig
|
|
},
|
|
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, {
|
|
global: false,
|
|
keyGenerator: () => 42,
|
|
allowList: (req, key) => {
|
|
t.assert.ok(req.headers)
|
|
t.assert.deepStrictEqual(key, 42)
|
|
return req.headers['x-my-header'] !== undefined
|
|
}
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: defaultRouteConfig
|
|
},
|
|
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, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: '2s',
|
|
onExceeding: function () {
|
|
t.assert.ok('onExceeding called')
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: '2s',
|
|
onExceeded: function () {
|
|
t.assert.ok('onExceeded called')
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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 keyGenerator', async (t) => {
|
|
t.plan(19)
|
|
const clock = mock.timers
|
|
clock.enable(0)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
keyGenerator: (req) => {
|
|
t.assert.deepStrictEqual(req.headers['my-custom-header'], 'random-value')
|
|
return req.headers['my-custom-header']
|
|
}
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: defaultRouteConfig
|
|
},
|
|
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('no rate limit without settings', async (t) => {
|
|
t.plan(3)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get('/', async () => 'hello!')
|
|
|
|
const res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], undefined)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], undefined)
|
|
})
|
|
|
|
test('no rate limit with bad rate-limit parameters', async (t) => {
|
|
t.plan(1)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
|
|
|
try {
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: Object.assign({}, defaultRouteConfig, { rateLimit: () => {} })
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
t.fail('should throw')
|
|
} catch (err) {
|
|
t.assert.deepStrictEqual(
|
|
err.message,
|
|
'Unknown value for route rate-limit configuration'
|
|
)
|
|
}
|
|
})
|
|
|
|
test('works with existing route config', async (t) => {
|
|
t.plan(2)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: defaultRouteConfig
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
await fastify.ready()
|
|
const res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '2')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '1')
|
|
})
|
|
|
|
test('With ban', async (t) => {
|
|
t.plan(3)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: { rateLimit: { max: 1, ban: 1 } }
|
|
},
|
|
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('route can disable the global limit', async (t) => {
|
|
t.plan(3)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { max: 2, timeWindow: 1000 })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: Object.assign({}, defaultRouteConfig, { rateLimit: false })
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
const res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], undefined)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], undefined)
|
|
})
|
|
|
|
test('does not override onRequest', async (t) => {
|
|
t.plan(4)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
onRequest: function (_req, _reply, next) {
|
|
t.assert.ok('onRequest called')
|
|
next()
|
|
},
|
|
config: defaultRouteConfig
|
|
},
|
|
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('onExceeding and onExceeded events', async (t) => {
|
|
t.plan(11)
|
|
|
|
let onExceedingCounter = 0
|
|
let onExceededCounter = 0
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: Object.assign({}, defaultRouteConfig, {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: 1000,
|
|
onExceeding: function (req) {
|
|
// it will be executed 2 times
|
|
t.assert.ok(req, 'req should be not null')
|
|
onExceedingCounter += 1
|
|
},
|
|
onExceeded: function (req) {
|
|
// it will be executed 2 times
|
|
t.assert.ok(req, 'req should be not null')
|
|
onExceededCounter += 1
|
|
}
|
|
}
|
|
})
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
const payload = { method: 'GET', url: '/' }
|
|
|
|
let res
|
|
|
|
res = await fastify.inject(payload)
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
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-remaining'], '0')
|
|
|
|
res = await fastify.inject(payload)
|
|
t.assert.deepStrictEqual(res.statusCode, 429)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
|
|
|
t.assert.deepStrictEqual(onExceedingCounter, 2)
|
|
t.assert.deepStrictEqual(onExceededCounter, 1)
|
|
})
|
|
|
|
test('custom error response', async (t) => {
|
|
t.plan(12)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
errorResponseBuilder: (_req, context) => ({
|
|
statusCode: 429,
|
|
timeWindow: context.after,
|
|
limit: context.max
|
|
})
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 2,
|
|
timeWindow: 1000
|
|
}
|
|
}
|
|
},
|
|
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(JSON.parse(res.payload), {
|
|
statusCode: 429,
|
|
timeWindow: '1 second',
|
|
limit: 2
|
|
})
|
|
})
|
|
|
|
test('variable max contenders', async (t) => {
|
|
t.plan(9)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
max: 1,
|
|
timeWindow: 10000
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
keyGenerator: (req) => req.headers['api-key'],
|
|
max: (_req, key) => (key === 'pro' ? 3 : 2)
|
|
}
|
|
}
|
|
},
|
|
async () => 'hello'
|
|
)
|
|
|
|
fastify.get(
|
|
'/limit',
|
|
{ config: { rateLimit: {} } },
|
|
async () => 'limited'
|
|
)
|
|
|
|
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': 'pro' }, status: 200, url: '/limit' },
|
|
{ headers: { 'api-key': 'pro' }, status: 429, url: '/limit' },
|
|
{ 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)
|
|
}
|
|
})
|
|
|
|
// // TODO this test gets extremely flaky because of setTimeout
|
|
// // rewrite using https://www.npmjs.com/package/@sinonjs/fake-timers
|
|
test('limit reset per Local storage', { skip: true }, async (t) => {
|
|
t.plan(12)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 4000
|
|
}
|
|
}
|
|
},
|
|
(_req, reply) => {
|
|
reply.send('hello!')
|
|
}
|
|
)
|
|
|
|
setTimeout(doRequest.bind(null, 4), 0)
|
|
setTimeout(doRequest.bind(null, 3), 1000)
|
|
setTimeout(doRequest.bind(null, 2), 2000)
|
|
setTimeout(doRequest.bind(null, 1), 3000)
|
|
setTimeout(doRequest.bind(null, 0), 4000)
|
|
setTimeout(doRequest.bind(null, 4), 4100)
|
|
|
|
function doRequest (resetValue) {
|
|
fastify.inject('/', (err, res) => {
|
|
t.error(err)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], resetValue)
|
|
})
|
|
}
|
|
})
|
|
|
|
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(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
timeWindow: 1000,
|
|
addHeaders: {
|
|
'x-ratelimit-limit': true, // this must override the global one
|
|
'x-ratelimit-remaining': false,
|
|
'x-ratelimit-reset': false,
|
|
'retry-after': false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
|
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.deepEqual(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(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
timeWindow: 1000,
|
|
addHeadersOnExceeding: {
|
|
'x-ratelimit-limit': true, // this must override the global one
|
|
'x-ratelimit-remaining': false,
|
|
'x-ratelimit-reset': false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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.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.deepStrictEqual(res.headers['x-ratelimit-limit'], '1')
|
|
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(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
timeWindow: 1000,
|
|
addHeaders: {
|
|
'x-ratelimit-limit': true, // this must override the global one
|
|
'x-ratelimit-remaining': false,
|
|
'x-ratelimit-reset': false,
|
|
'retry-after': false
|
|
},
|
|
addHeadersOnExceeding: {
|
|
'x-ratelimit-limit': false,
|
|
'x-ratelimit-remaining': true, // this must override the global one
|
|
'x-ratelimit-reset': false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
|
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.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.deepStrictEqual(res.headers['x-ratelimit-remaining'], '0')
|
|
t.assert.notStrictEqual(
|
|
res.headers['x-ratelimit-reset'],
|
|
'the header must be missing'
|
|
)
|
|
|
|
clock.reset()
|
|
})
|
|
|
|
test('global timeWindow when not set in routes', async (t) => {
|
|
t.plan(4)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
timeWindow: 6000
|
|
})
|
|
|
|
fastify.get(
|
|
'/six',
|
|
{
|
|
config: { rateLimit: { max: 6 } }
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
fastify.get(
|
|
'/four',
|
|
{
|
|
config: { rateLimit: { max: 4, timeWindow: 4000 } }
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
let res
|
|
|
|
res = await fastify.inject('/six')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '6')
|
|
|
|
res = await fastify.inject('/four')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-reset'], '4')
|
|
})
|
|
|
|
test('timeWindow specified as a string', async (t) => {
|
|
t.plan(9)
|
|
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))
|
|
return store
|
|
}
|
|
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
store: CustomStore
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: { rateLimit: { max: 2, timeWindow: '10 seconds' } }
|
|
},
|
|
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)
|
|
})
|
|
|
|
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))
|
|
return store
|
|
}
|
|
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
max: 1,
|
|
timeWindow: 10000,
|
|
store: CustomStore
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: { rateLimit: { max: 2, timeWindow: 10000 } }
|
|
},
|
|
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('stops fastify lifecycle after onRequest and before preValidation', async (t) => {
|
|
t.plan(4)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
let preValidationCallCount = 0
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 1000
|
|
}
|
|
},
|
|
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('avoid double onRequest', async (t) => {
|
|
t.plan(3)
|
|
|
|
const fastify = Fastify()
|
|
|
|
let keyGeneratorCallCount = 0
|
|
|
|
const subroute = async (childServer) => {
|
|
await childServer.register(rateLimit, {
|
|
max: 1,
|
|
timeWindow: 1000,
|
|
keyGenerator: (req) => {
|
|
t.assert.ok('keyGenerator called only once')
|
|
keyGeneratorCallCount++
|
|
|
|
return req.ip
|
|
}
|
|
})
|
|
|
|
childServer.get('/', {}, async () => 'hello!')
|
|
}
|
|
|
|
fastify.register(subroute, { prefix: '/test' })
|
|
|
|
const res = await fastify.inject({
|
|
url: '/test',
|
|
method: 'GET'
|
|
})
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(keyGeneratorCallCount, 1)
|
|
})
|
|
|
|
test('Allow multiple different rate limiter registrations', async (t) => {
|
|
t.plan(16)
|
|
const fastify = Fastify()
|
|
|
|
await fastify.register(rateLimit, {
|
|
max: 1,
|
|
timeWindow: 1000,
|
|
whitelist: (req) => req.url !== '/test'
|
|
})
|
|
|
|
await fastify.register(rateLimit, {
|
|
max: 1,
|
|
timeWindow: 1000,
|
|
whitelist: (req) => req.url === '/test'
|
|
})
|
|
|
|
fastify.get('/', async () => 'hello!')
|
|
|
|
fastify.get('/test', async () => 'hello from another route!')
|
|
|
|
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')
|
|
|
|
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.deepStrictEqual(res.headers['retry-after'], '1')
|
|
|
|
res = await fastify.inject('/test')
|
|
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('/test')
|
|
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['retry-after'], '1')
|
|
})
|
|
|
|
test('With enable IETF draft spec', async (t) => {
|
|
t.plan(4)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
enableDraftSpec: true
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: defaultRouteConfig
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
const 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')
|
|
t.assert.deepStrictEqual(res.headers['ratelimit-reset'], '1')
|
|
})
|
|
|
|
test('per route rate limit', async (t) => {
|
|
const fastify = Fastify({
|
|
exposeHeadRoutes: true
|
|
})
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 10,
|
|
timeWindow: 1000
|
|
}
|
|
}
|
|
},
|
|
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 (per route limit)'
|
|
)
|
|
t.assert.deepStrictEqual(
|
|
res.headers['x-ratelimit-remaining'],
|
|
'9',
|
|
'GET: x-ratelimit-remaining header (per route 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 (per route limit)'
|
|
)
|
|
t.assert.deepStrictEqual(
|
|
resHead.headers['x-ratelimit-remaining'],
|
|
'9',
|
|
'HEAD: x-ratelimit-remaining header (per route limit)'
|
|
)
|
|
})
|
|
|
|
test('Allow custom timeWindow in preHandler', async (t) => {
|
|
t.plan(23)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
fastify.register((fastify, _options, done) => {
|
|
fastify.get(
|
|
'/default',
|
|
{
|
|
config: { rateLimit: { max: 1, timeWindow: '10 seconds' } }
|
|
},
|
|
async () =>
|
|
'Global rateLimiter should limit this with 60seconds timeWindow'
|
|
)
|
|
fastify.route({
|
|
method: 'GET',
|
|
url: '/2',
|
|
preHandler: [
|
|
fastify.rateLimit({
|
|
max: 1,
|
|
timeWindow: '2 minutes',
|
|
keyGenerator: () => 245
|
|
})
|
|
],
|
|
|
|
handler: async () => ({ hello: 'world' })
|
|
})
|
|
|
|
fastify.route({
|
|
method: 'GET',
|
|
url: '/3',
|
|
preHandler: [
|
|
fastify.rateLimit({
|
|
max: 1,
|
|
timeWindow: '3 minutes',
|
|
keyGenerator: () => 345
|
|
})
|
|
],
|
|
|
|
handler: async () => ({ hello: 'world' })
|
|
})
|
|
|
|
done()
|
|
})
|
|
|
|
let res = await fastify.inject('/2')
|
|
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('/2')
|
|
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'], '120')
|
|
t.assert.deepStrictEqual(res.headers['retry-after'], '120')
|
|
|
|
res = await fastify.inject('/3')
|
|
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('/3')
|
|
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'], '180')
|
|
t.assert.deepStrictEqual(res.headers['retry-after'], '180')
|
|
|
|
res = await fastify.inject('/default')
|
|
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('/default')
|
|
t.assert.deepStrictEqual(res.headers['retry-after'], '10')
|
|
t.assert.deepStrictEqual(res.statusCode, 429)
|
|
})
|
|
|
|
test('When continue exceeding is on (Local)', async (t) => {
|
|
const fastify = Fastify()
|
|
|
|
await fastify.register(rateLimit, {
|
|
global: false
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 5000,
|
|
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')
|
|
})
|
|
|
|
test('should consider routes allow list', async (t) => {
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: { allowList: ['127.0.0.1'], max: 2, timeWindow: 10000 }
|
|
}
|
|
},
|
|
(_req, reply) => {
|
|
reply.send('hello!')
|
|
}
|
|
)
|
|
|
|
let 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('on preValidation hook', async (t) => {
|
|
const fastify = Fastify()
|
|
|
|
await fastify.register(rateLimit, {
|
|
global: false
|
|
})
|
|
|
|
fastify.get(
|
|
'/quero',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 10000,
|
|
hook: 'preValidation',
|
|
keyGenerator (req) {
|
|
return req.userId || req.ip
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => 'fastify is awesome !'
|
|
)
|
|
|
|
fastify.decorateRequest('userId', '')
|
|
fastify.addHook('preParsing', async (req) => {
|
|
const { userId } = req.query
|
|
if (userId) {
|
|
req.userId = userId
|
|
}
|
|
})
|
|
|
|
const send = (userId) => {
|
|
let query
|
|
if (userId) {
|
|
query = { userId }
|
|
}
|
|
return fastify.inject({
|
|
url: '/quero',
|
|
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('on undefined hook should use onRequest-hook', async (t) => {
|
|
t.plan(2)
|
|
const fastify = Fastify()
|
|
|
|
await fastify.register(rateLimit, {
|
|
global: false
|
|
})
|
|
|
|
fastify.addHook('onRoute', function (routeOptions) {
|
|
t.assert.deepStrictEqual(routeOptions.preHandler, undefined)
|
|
t.assert.deepStrictEqual(routeOptions.onRequest.length, 1)
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
exposeHeadRoute: false,
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 10000,
|
|
hook: 'onRequest'
|
|
}
|
|
}
|
|
},
|
|
async () => 'fastify is awesome !'
|
|
)
|
|
})
|
|
|
|
test('on rateLimitHook should not be set twice on HEAD', async (t) => {
|
|
const fastify = Fastify()
|
|
|
|
await fastify.register(rateLimit, {
|
|
global: false
|
|
})
|
|
|
|
fastify.addHook('onRoute', function (routeOptions) {
|
|
t.assert.deepStrictEqual(routeOptions.preHandler, undefined)
|
|
t.assert.deepStrictEqual(routeOptions.onRequest.length, 1)
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
exposeHeadRoute: true,
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 10000,
|
|
hook: 'onRequest'
|
|
}
|
|
}
|
|
},
|
|
async () => 'fastify is awesome !'
|
|
)
|
|
|
|
fastify.head(
|
|
'/explicit-head',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 10000,
|
|
hook: 'onRequest'
|
|
}
|
|
}
|
|
},
|
|
async () => 'fastify is awesome !'
|
|
)
|
|
|
|
fastify.head(
|
|
'/explicit-head-2',
|
|
{
|
|
exposeHeadRoute: true,
|
|
config: {
|
|
rateLimit: {
|
|
max: 1,
|
|
timeWindow: 10000,
|
|
hook: 'onRequest'
|
|
}
|
|
}
|
|
},
|
|
async () => 'fastify is awesome !'
|
|
)
|
|
})
|
|
|
|
test("child's allowList should not crash the app", async (t) => {
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
allowList: () => false
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: { allowList: ['127.0.0.1'], max: 2, timeWindow: 10000 }
|
|
}
|
|
},
|
|
(_req, reply) => {
|
|
reply.send('hello!')
|
|
}
|
|
)
|
|
|
|
let 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("child's allowList function should not crash and should override parent", async (t) => {
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
allowList: ['127.0.0.1']
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: { allowList: () => false, max: 2, timeWindow: 10000 }
|
|
}
|
|
},
|
|
(_req, reply) => {
|
|
reply.send('hello!')
|
|
}
|
|
)
|
|
|
|
let 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('rateLimit decorator should work when a property other than timeWindow is modified', async (t) => {
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, {
|
|
global: false,
|
|
allowList: () => false
|
|
})
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
onRequest: fastify.rateLimit({
|
|
allowList: ['127.0.0.1'],
|
|
max: 1
|
|
})
|
|
},
|
|
(_req, reply) => {
|
|
reply.send('hello!')
|
|
}
|
|
)
|
|
|
|
let 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)
|
|
|
|
res = await fastify.inject({
|
|
path: '/',
|
|
remoteAddress: '1.1.1.1'
|
|
})
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
|
|
res = await fastify.inject({
|
|
path: '/',
|
|
remoteAddress: '1.1.1.1'
|
|
})
|
|
t.assert.deepStrictEqual(res.statusCode, 429)
|
|
})
|
|
|
|
test('With NaN in subroute config', async (t) => {
|
|
t.plan(12)
|
|
const clock = mock.timers
|
|
clock.enable(0)
|
|
const fastify = Fastify()
|
|
await fastify.register(rateLimit, { global: false })
|
|
|
|
fastify.get(
|
|
'/',
|
|
{
|
|
config: {
|
|
rateLimit: {
|
|
max: NaN
|
|
}
|
|
}
|
|
},
|
|
async () => 'hello!'
|
|
)
|
|
|
|
let res
|
|
|
|
res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '999')
|
|
|
|
res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '998')
|
|
|
|
res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '997')
|
|
|
|
clock.tick(70000)
|
|
|
|
res = await fastify.inject('/')
|
|
t.assert.deepStrictEqual(res.statusCode, 200)
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-limit'], '1000')
|
|
t.assert.deepStrictEqual(res.headers['x-ratelimit-remaining'], '999')
|
|
|
|
clock.reset()
|
|
})
|