'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() })