Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fastify-rate-limit

Package Overview
Dependencies
Maintainers
8
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fastify-rate-limit - npm Package Compare versions

Comparing version 2.4.0 to 3.0.0

example/example-custom.js

19

index.d.ts

@@ -5,2 +5,11 @@ import * as http from 'http';

declare namespace fastifyRateLimit {
interface FastifyRateLimitStoreCtor {
new (options: FastifyRateLimitOptions): FastifyRateLimitStore;
}
interface FastifyRateLimitStore {
incr(key: string, callback: ( error: Error|null, result?: { current: number, ttl: number } ) => void): void;
child(routeOptions: fastify.RouteOptions<http.Server, http.IncomingMessage, http.ServerResponse> & { path: string, prefix: string }): FastifyRateLimitStore;
}
interface FastifyRateLimitOptions {

@@ -11,7 +20,10 @@ global?: boolean;

cache?: number;
store?: FastifyRateLimitStoreCtor;
whitelist?: string[] | ((req: fastify.FastifyRequest<http.IncomingMessage>, key: string) => boolean);
redis?: any;
skipOnError?: boolean;
ban?: number;
keyGenerator?: (req: fastify.FastifyRequest<http.IncomingMessage>) => string | number;
errorResponseBuilder?: (req: fastify.FastifyRequest<http.IncomingMessage>, context: errorResponseBuilderContext) => object;
addHeaders?: AddHeaders;
}

@@ -23,2 +35,9 @@

}
interface AddHeaders {
'x-ratelimit-limit'?: boolean,
'x-ratelimit-remaining'?: boolean,
'x-ratelimit-reset'?: boolean,
'retry-after'?: boolean
}
}

@@ -25,0 +44,0 @@

56

index.js

@@ -26,2 +26,9 @@ 'use strict'

globalParams.addHeaders = Object.assign({
'x-ratelimit-limit': true,
'x-ratelimit-remaining': true,
'x-ratelimit-reset': true,
'retry-after': true
}, settings.addHeaders)
// define the global maximum of request allowed

@@ -40,2 +47,3 @@ globalParams.max = (typeof settings.max === 'number' || typeof settings.max === 'function')

globalParams.whitelist = settings.whitelist || null
globalParams.ban = settings.ban || null

@@ -47,6 +55,11 @@ // define the name of the app component. Related to redis, it will be use as a part of the keyname define in redis.

if (settings.redis) {
pluginComponent.store = new RedisStore(settings.redis, 'fastify-rate-limit-', globalParams.timeWindow)
if (settings.store) {
const Store = settings.store
pluginComponent.store = new Store(globalParams)
} else {
pluginComponent.store = new LocalStore(globalParams.timeWindow, settings.cache, fastify)
if (settings.redis) {
pluginComponent.store = new RedisStore(settings.redis, 'fastify-rate-limit-', globalParams.timeWindow)
} else {
pluginComponent.store = new LocalStore(globalParams.timeWindow, settings.cache, fastify)
}
}

@@ -58,3 +71,3 @@

globalParams.errorResponseBuilder = (req, context) => ({ statusCode: 429, error: 'Too Many Requests', message: `Rate limit exceeded, retry in ${context.after}` })
globalParams.errorResponseBuilder = (req, context) => ({ statusCode: context.statusCode, error: 'Too Many Requests', message: `Rate limit exceeded, retry in ${context.after}` })
globalParams.isCustomErrorMessage = false

@@ -73,5 +86,10 @@

const current = Object.create(pluginComponent)
const mergedRateLimitParams = makeParams(routeOptions.config.rateLimit)
if (!routeOptions.config.rateLimit.timeWindow) {
// load the global timewindow if it is missing from the route config
routeOptions.config.rateLimit.timeWindow = mergedRateLimitParams.timeWindow
}
current.store = pluginComponent.store.child(routeOptions)
// if the current endpoint have a custom rateLimit configuration ...
buildRouteRate(current, makeParams(routeOptions.config.rateLimit), routeOptions)
buildRouteRate(current, mergedRateLimitParams, routeOptions)
} else if (routeOptions.config.rateLimit === false) {

@@ -133,3 +151,3 @@ // don't apply any rate-limit

function onIncr (err, current) {
function onIncr (err, { current, ttl }) {
if (err && params.skipOnError === false) {

@@ -142,3 +160,4 @@ return next(err)

res.header('x-ratelimit-limit', maximum)
res.header('x-ratelimit-remaining', maximum - current)
.header('x-ratelimit-remaining', maximum - current)
.header('x-ratelimit-reset', Math.floor(ttl / 1000))

@@ -159,7 +178,20 @@ if (typeof params.onExceeding === 'function') {

res.code(429)
.header('x-ratelimit-limit', maximum)
.header('x-ratelimit-remaining', 0)
.header('retry-after', params.timeWindow)
.send(params.errorResponseBuilder(req, { after, max: maximum }))
if (params.addHeaders['x-ratelimit-limit']) { res.header('x-ratelimit-limit', maximum) }
if (params.addHeaders['x-ratelimit-remaining']) { res.header('x-ratelimit-remaining', 0) }
if (params.addHeaders['x-ratelimit-reset']) { res.header('x-ratelimit-reset', Math.floor(ttl / 1000)) }
if (params.addHeaders['retry-after']) { res.header('retry-after', params.timeWindow) }
const code = params.ban && current - maximum > params.ban ? 403 : 429
res.code(code)
const respCtx = {
statusCode: code,
after,
max: maximum
}
if (code === 403) {
respCtx.ban = true
}
res.send(params.errorResponseBuilder(req, respCtx))
}

@@ -166,0 +198,0 @@

{
"name": "fastify-rate-limit",
"version": "2.4.0",
"version": "3.0.0",
"description": "A low overhead rate limiter for your routes",

@@ -28,5 +28,7 @@ "main": "index.js",

"@types/ioredis": "~4.0.11",
"@types/node": "~12.7.3",
"@types/node": "~12.11.0",
"fastify": "^2.5.0",
"ioredis": "^4.9.0",
"knex": "^0.20.2",
"sqlite3": "^4.1.0",
"standard": "^14.0.2",

@@ -33,0 +35,0 @@ "tap": "^12.6.6",

@@ -53,2 +53,3 @@ # fastify-rate-limit

|`x-ratelimit-remaining` | how many request remain to the client in the timewindow
|`x-ratelimit-reset` | how many seconds must pass before the rate limit resets
|`retry-after` | if the max has been reached, the millisecond the client must wait before perform new requests

@@ -63,2 +64,3 @@

max: 3, // default 1000
ban: 2, // default null
timeWindow: 5000, // default 1000 * 60

@@ -70,3 +72,9 @@ cache: 10000, // default 5000

keyGenerator: function(req) { /* ... */ }, // default (req) => req.raw.ip
errorResponseBuilder: function(req, context) { /* ... */},
errorResponseBuilder: function(req, context) { /* ... */},
addHeaders: { // default show all the response headers when rate limit is reached
'x-ratelimit-limit': true,
'x-ratelimit-remaining': true,
'x-ratelimit-reset': true,
'retry-after': true
}
})

@@ -77,2 +85,3 @@ ```

- `max`: is the maximum number of requests a single client can perform inside a timeWindow. It can be a sync function with the signature `(req, key) => {}` where `req` is the Fastify request object and `key` is the value generated by the `keyGenerator`. The function **must** return a number.
- `ban`: is the maximum number of 429 responses to return to a single client before returning 403. When the ban limit is exceeded the context field will have `ban=true` in the errorResponseBuilder. This parameter is an in-memory counter and could not work properly in a distributed environment.
- `timeWindow:` the duration of the time window. It can be expressed in milliseconds or as a string (in the [`ms`](https://github.com/zeit/ms) format)

@@ -83,5 +92,7 @@ - `cache`: this plugin internally uses a lru cache to handle the clients, you can change the size of the cache with this option

You can pass a Redis client here and magically the issue is solved. To achieve the maximum speed, this plugins requires the use of [`ioredis`](https://github.com/luin/ioredis)
- `store`: a custom store to track requests and rates which allows you to use your own storage mechanism (such as using an RDBMS, MongoDB, etc...) as well as further customizing the logic used in calculating the rate limits. A simple example is provided below as well as a more detailed example using Knex.js can be found in the [`example/`](https://github.com/fastify/fastify-rate-limit/tree/master/example) folder
- `skipOnError`: if `true` it will skip errors generated by the storage (eg, redis not reachable).
- `keyGenerator`: a function to generate a unique identifier for each incoming request. Defaults to `(req) => req.ip`, the IP is resolved by fastify using `req.connection.remoteAddress` or `req.headers['x-forwarded-for']` if [trustProxy](https://www.fastify.io/docs/master/Server/#trustproxy) option is enabled. Use it if you want to override this behavior
- `errorResponseBuilder`: a function to generate a custom response object. Defaults to `(req, context) => ({statusCode: 429, error: 'Too Many Requests', message: ``Rate limit exceeded, retry in ${context.after}``})`
- `addHeaders`: define which headers should be added in the response when the limit is reached. Defaults all the headers will be shown

@@ -137,2 +148,32 @@ `keyGenerator` example usage:

Custom `store` example usage:
```js
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) {
// We create a merged copy of the current parent parameters with the specific
// route parameters and pass them into the child store.
const childParams = Object.assign(this.options, routeOptions.config.rateLimit)
const store = new CustomStore(childParams)
// Here is where you may want to do some custom calls on the store with the information
// in routeOptions first...
// store.setSubKey(routeOptions.method + routeOptions.url)
return store
}
fastify.register(require('fastify-rate-limit'), {
/* ... */
store: CustomStore
})
```
### Options on the endpoint itself

@@ -139,0 +180,0 @@

'use strict'
const lru = require('tiny-lru')
const ms = require('ms')
function LocalStore (timeWindow, cache, app) {
this.lru = lru(cache || 5000)
this.interval = setInterval(this.lru.clear.bind(this.lru), timeWindow).unref()
this.interval = setInterval(beat.bind(this), timeWindow).unref()
this.app = app
this.timeWindow = timeWindow

@@ -14,2 +14,7 @@ app.addHook('onClose', (done) => {

})
function beat () {
this.lru.clear()
this.msLastBeat = null
}
}

@@ -20,14 +25,16 @@

this.lru.set(ip, ++current)
cb(null, current)
}
LocalStore.prototype.child = function (routeOptions) {
let timeWindow = routeOptions.config.rateLimit.timeWindow
if (typeof timeWindow === 'string') {
timeWindow = ms(timeWindow)
// start counting from the first request/increment
if (!this.msLastBeat) {
this.msLastBeat = Date.now()
}
return new LocalStore(timeWindow, routeOptions.config.rateLimit.cache, this.app)
cb(null, { current, ttl: this.timeWindow - (Date.now() - this.msLastBeat) })
}
LocalStore.prototype.child = function (routeOptions) {
return new LocalStore(routeOptions.config.rateLimit.timeWindow,
routeOptions.config.rateLimit.cache, this.app)
}
module.exports = LocalStore
'use strict'
const ms = require('ms')
const noop = () => {}

@@ -18,8 +17,13 @@

.exec((err, result) => {
if (err) return cb(err, 0)
if (result[0][0]) return cb(result[0][0], 0)
/**
* result[0] => incr response: [0]: error, [1]: new incr value
* result[1] => pttl response: [0]: error, [1]: ttl remaining
*/
if (err) return cb(err, { current: 0 })
if (result[0][0]) return cb(result[0][0], { current: 0 })
if (result[1][1] === -1) {
this.redis.pexpire(key, this.timeWindow, noop)
result[1][1] = this.timeWindow
}
cb(null, result[0][1])
cb(null, { current: result[0][1], ttl: result[1][1] })
})

@@ -29,9 +33,5 @@ }

RedisStore.prototype.child = function (routeOptions) {
let timeWindow = routeOptions.config.rateLimit.timeWindow
if (typeof timeWindow === 'string') {
timeWindow = ms(timeWindow)
}
const child = Object.create(this)
child.key = this.key + routeOptions.method + routeOptions.url + '-'
child.timeWindow = timeWindow
child.timeWindow = routeOptions.config.rateLimit.timeWindow
return child

@@ -38,0 +38,0 @@ }

@@ -197,3 +197,3 @@ 'use strict'

test('With redis store', t => {
t.plan(19)
t.plan(23)
const fastify = Fastify()

@@ -216,2 +216,3 @@ const redis = new Redis({ host: REDIS_HOST })

t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)

@@ -223,2 +224,3 @@ fastify.inject('/', (err, res) => {

t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 0)

@@ -231,2 +233,3 @@ fastify.inject('/', (err, res) => {

t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 0)
t.strictEqual(res.headers['retry-after'], 1000)

@@ -252,2 +255,3 @@ t.deepEqual({

t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
})

@@ -363,2 +367,63 @@ }

test('With CustomStore', t => {
t.plan(18)
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()
fastify.register(rateLimit, {
max: 2,
timeWindow: 10000,
store: CustomStore
})
fastify.get('/', (req, reply) => {
reply.send('hello!')
})
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 2)
t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 9)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 2)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 8)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 429)
t.strictEqual(res.headers['content-type'], 'application/json')
t.strictEqual(res.headers['x-ratelimit-limit'], 2)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 7)
t.strictEqual(res.headers['retry-after'], 10000)
t.deepEqual({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 10 seconds'
}, JSON.parse(res.payload))
})
})
})
})
test('does not override the preHandler', t => {

@@ -470,1 +535,76 @@ t.plan(5)

})
test('hide rate limit headers', t => {
t.plan(17)
const fastify = Fastify()
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('/', (req, res) => { res.send('hello') })
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 1)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 429)
t.strictEqual(res.headers['content-type'], 'application/json')
t.notOk(res.headers['x-ratelimit-limit'], 'the header must be missing')
t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing')
t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing')
t.notOk(res.headers['retry-after'], 'the header must be missing')
setTimeout(retry, 1100)
})
})
function retry () {
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 1)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
})
}
})
test('With ban', t => {
t.plan(6)
const fastify = Fastify()
fastify.register(rateLimit, {
max: 1,
ban: 1
})
fastify.get('/', (req, reply) => {
reply.send('hello!')
})
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 429)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 403)
})
})
})
})

@@ -23,3 +23,3 @@ 'use strict'

test('Basic', t => {
t.plan(19)
t.plan(23)
const fastify = Fastify()

@@ -39,2 +39,3 @@ fastify.register(rateLimit, { global: false })

t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)

@@ -46,2 +47,3 @@ fastify.inject('/', (err, res) => {

t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 0)

@@ -55,2 +57,3 @@ fastify.inject('/', (err, res) => {

t.strictEqual(res.headers['retry-after'], 1000)
t.strictEqual(res.headers['x-ratelimit-reset'], 0)
t.deepEqual({

@@ -73,2 +76,3 @@ statusCode: 429,

t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
})

@@ -218,3 +222,3 @@ }

test('With redis store', t => {
t.plan(19)
t.plan(23)
const fastify = Fastify()

@@ -238,2 +242,3 @@ const redis = new Redis({ host: REDIS_HOST })

t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)

@@ -245,2 +250,3 @@ fastify.inject('/', (err, res) => {

t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 0)

@@ -254,2 +260,3 @@ fastify.inject('/', (err, res) => {

t.strictEqual(res.headers['retry-after'], 1000)
t.strictEqual(res.headers['x-ratelimit-reset'], 0)
t.deepEqual({

@@ -274,2 +281,3 @@ statusCode: 429,

t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
})

@@ -441,2 +449,31 @@ }

test('With ban', t => {
t.plan(6)
const fastify = Fastify()
fastify.register(rateLimit, {
global: false
})
fastify.get('/', {
config: { rateLimit: { max: 1, ban: 1 } }
}, (req, reply) => {
reply.send('hello!')
})
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 429)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 403)
})
})
})
})
test('route can disable the global limit', t => {

@@ -633,1 +670,188 @@ t.plan(4)

})
test('limit reset per Local storage', t => {
t.plan(12)
const fastify = Fastify()
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.strictEqual(res.headers['x-ratelimit-reset'], resetValue)
})
}
})
test('hide rate limit headers', t => {
t.plan(17)
const fastify = Fastify()
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
}
}
}
}, (req, res) => { res.send('hello') })
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 1)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 429)
t.strictEqual(res.headers['content-type'], 'application/json')
t.strictEqual(res.headers['x-ratelimit-limit'], 1)
t.notOk(res.headers['x-ratelimit-remaining'], 'the header must be missing')
t.notOk(res.headers['x-ratelimit-reset'], 'the header must be missing')
t.notOk(res.headers['retry-after'], 'the header must be missing')
setTimeout(retry, 1100)
})
})
function retry () {
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 1)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 1)
})
}
})
test('global timeWindow when not set in routes', t => {
t.plan(6)
const fastify = Fastify()
fastify.register(rateLimit, {
global: false,
timeWindow: 6000
})
fastify.get('/six', {
config: { rateLimit: { max: 6 } }
}, (req, reply) => {
reply.send('hello!')
})
fastify.get('/four', {
config: { rateLimit: { max: 4, timeWindow: 4000 } }
}, (req, reply) => {
reply.send('hello!')
})
fastify.inject('/six', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-reset'], 6)
fastify.inject('/four', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-reset'], 4)
})
})
})
test('With CustomStore', t => {
t.plan(18)
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()
fastify.register(rateLimit, {
global: false,
max: 1,
timeWindow: 10000,
store: CustomStore
})
fastify.get('/', {
config: { rateLimit: { max: 2, timeWindow: 10000 } }
}, (req, reply) => {
reply.send('hello!')
})
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 2)
t.strictEqual(res.headers['x-ratelimit-remaining'], 1)
t.strictEqual(res.headers['x-ratelimit-reset'], 9)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 200)
t.strictEqual(res.headers['x-ratelimit-limit'], 2)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 8)
fastify.inject('/', (err, res) => {
t.error(err)
t.strictEqual(res.statusCode, 429)
t.strictEqual(res.headers['content-type'], 'application/json')
t.strictEqual(res.headers['x-ratelimit-limit'], 2)
t.strictEqual(res.headers['x-ratelimit-remaining'], 0)
t.strictEqual(res.headers['x-ratelimit-reset'], 7)
t.strictEqual(res.headers['retry-after'], 10000)
t.deepEqual({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 10 seconds'
}, JSON.parse(res.payload))
})
})
})
})
import * as http from 'http'
import * as fastify from 'fastify';
import * as types from '../../index';
import * as fastifyRateLimit from '../../../fastify-rate-limit';

@@ -16,4 +17,11 @@ import * as ioredis from 'ioredis';

skipOnError: true,
ban: 10,
keyGenerator: (req: fastify.FastifyRequest<http.IncomingMessage>) => req.ip,
errorResponseBuilder: (req: fastify.FastifyRequest<http.IncomingMessage>, context) => ({ code: 429, timeWindow: context.after, limit: context.max })
errorResponseBuilder: (req: fastify.FastifyRequest<http.IncomingMessage>, context) => ({ code: 429, timeWindow: context.after, limit: context.max }),
addHeaders: {
'x-ratelimit-limit': false,
'x-ratelimit-remaining': false,
'x-ratelimit-reset': false,
'retry-after': false
}
});

@@ -27,1 +35,16 @@

});
class CustomStore implements types.FastifyRateLimitStore {
constructor(options: types.FastifyRateLimitOptions) {}
incr(key: string, callback: ( error: Error|null, result?: { current: number, ttl: number } ) => void) {}
child(routeOptions: fastify.RouteOptions<http.Server, http.IncomingMessage, http.ServerResponse> & { path: string, prefix: string }) {
return <CustomStore>(<types.FastifyRateLimitOptions>{})
}
}
app.register(fastifyRateLimit, {
global: true,
max: (req: fastify.FastifyRequest<http.IncomingMessage>, key: string) => (42),
timeWindow: 5000,
store: CustomStore
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc