Buckets:
ktongue/docker_container / .cache /opencode /node_modules /@fastify /rate-limit /test /route-rate-limit.test.js
| 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() | |
| }) | |
Xet Storage Details
- Size:
- 44.9 kB
- Xet hash:
- 0e8db52bb1a1bca5d64226d7cc8843a25e2ea50172146db3a3eb7fb83756c181
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.