fastify-rate-limit
Advanced tools
Comparing version 0.2.0 to 0.3.0
68
index.js
'use strict' | ||
const fp = require('fastify-plugin') | ||
const lru = require('tiny-lru') | ||
const FJS = require('fast-json-stringify') | ||
const ms = require('ms') | ||
const LocalStore = require('./store/LocalStore') | ||
const RedisStore = require('./store/RedisStore') | ||
const serializeError = FJS({ | ||
@@ -18,15 +20,17 @@ type: 'object', | ||
function rateLimitPlugin (fastify, opts, next) { | ||
const cache = lru(opts.cache || 5000) | ||
const max = opts.max || 1000 | ||
const whitelist = opts.whitelist || [] | ||
const timeWindow = typeof opts.timeWindow === 'string' | ||
? ms(opts.timeWindow) | ||
: typeof opts.timeWindow === 'number' | ||
? opts.timeWindow | ||
: 1000 * 60 | ||
? opts.timeWindow | ||
: 1000 * 60 | ||
const store = opts.redis | ||
? new RedisStore(opts.redis, timeWindow) | ||
: new LocalStore(timeWindow, opts.cache) | ||
const skipOnError = opts.skipOnError === true | ||
const max = opts.max || 1000 | ||
const whitelist = opts.whitelist || [] | ||
const after = ms(timeWindow, { long: true }) | ||
const interval = setInterval(cache.reset.bind(cache), timeWindow) | ||
if (interval.unref) interval.unref() | ||
fastify.addHook('onRequest', onRateLimit) | ||
@@ -36,25 +40,29 @@ | ||
var ip = req.headers['X-Forwarded-For'] || req.connection.remoteAddress | ||
var current = cache.get(ip) || 0 | ||
if (whitelist.indexOf(ip) > -1) { | ||
next() | ||
} else { | ||
store.incr(ip, onIncr) | ||
} | ||
var limitReached = current >= max | ||
function onIncr (err, current) { | ||
if (err && skipOnError === false) return next(err) | ||
if (whitelist.indexOf(ip) === -1) { | ||
if (limitReached === false) current++ | ||
cache.set(ip, current) | ||
res.setHeader('X-RateLimit-Limit', max) | ||
res.setHeader('X-RateLimit-Remaining', max - current) | ||
if (current <= max) { | ||
res.setHeader('X-RateLimit-Limit', max) | ||
res.setHeader('X-RateLimit-Remaining', max - current) | ||
next() | ||
} else { | ||
res.writeHead(429, { | ||
'X-RateLimit-Limit': max, | ||
'X-RateLimit-Remaining': 0, | ||
'Content-Type': 'application/json', | ||
'Retry-After': timeWindow | ||
}) | ||
res.end(serializeError({ | ||
statusCode: 429, | ||
error: 'Too Many Requests', | ||
message: `Rate limit exceeded, retry in ${after}` | ||
})) | ||
} | ||
} | ||
if (limitReached === false) { | ||
next() | ||
} else { | ||
res.setHeader('Content-Type', 'application/json') | ||
res.setHeader('Retry-After', timeWindow) | ||
res.statusCode = 429 | ||
res.end(serializeError({ | ||
statusCode: 429, | ||
error: 'Too Many Requests', | ||
message: `Rate limit exceeded, retry in ${after}` | ||
})) | ||
} | ||
} | ||
@@ -66,4 +74,4 @@ | ||
module.exports = fp(rateLimitPlugin, { | ||
fastify: '>=0.43.0', | ||
fastify: '>=1.x', | ||
name: 'fastify-rate-limit' | ||
}) |
{ | ||
"name": "fastify-rate-limit", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "A low overhead rate limiter for your routes", | ||
"main": "index.js", | ||
"scripts": { | ||
"redis": "docker run -p 6379:6379 --rm redis:3.0.7", | ||
"test": "standard && tap test.js" | ||
@@ -25,12 +26,13 @@ }, | ||
"devDependencies": { | ||
"fastify": "^1.1.1", | ||
"standard": "^10.0.3", | ||
"tap": "^11.1.0" | ||
"fastify": "^1.2.1", | ||
"ioredis": "^3.2.2", | ||
"standard": "^11.0.1", | ||
"tap": "^11.1.3" | ||
}, | ||
"dependencies": { | ||
"fast-json-stringify": "^1.1.0", | ||
"fast-json-stringify": "^1.2.0", | ||
"fastify-plugin": "^0.2.2", | ||
"ms": "^2.1.1", | ||
"tiny-lru": "^1.5.0" | ||
"tiny-lru": "^1.5.2" | ||
} | ||
} |
@@ -50,3 +50,5 @@ # fastify-rate-limit | ||
cache: 10000, // default 5000 | ||
whitelist: ['127.0.0.1'] // default [] | ||
whitelist: ['127.0.0.1'], // default [] | ||
redis: new Redis({ host: '127.0.0.1' }), // default null | ||
skipOnError: true // default false | ||
}) | ||
@@ -58,2 +60,5 @@ ``` | ||
- `whitelist`: array of string of ips to exlude from rate limiting | ||
- `redis`: by default this plugins uses an in-memory store, which is fast but if you application works on more than one server it is useless, since the data is store locally.<br> | ||
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). | ||
- `skipOnError`: if `true` it will skip errors generated by the storage (eg, redis not reachable) | ||
@@ -60,0 +65,0 @@ <a name="license"></a> |
100
test.js
@@ -5,4 +5,6 @@ 'use strict' | ||
const test = t.test | ||
const Redis = require('ioredis') | ||
const Fastify = require('fastify') | ||
const rateLimit = require('./index') | ||
const noop = () => {} | ||
@@ -131,1 +133,99 @@ test('Basic', t => { | ||
}) | ||
test('With redis store', t => { | ||
t.plan(19) | ||
const fastify = Fastify() | ||
const redis = new Redis({ host: '127.0.0.1' }) | ||
fastify.register(rateLimit, { | ||
max: 2, | ||
timeWindow: 1000, | ||
redis: redis | ||
}) | ||
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) | ||
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) | ||
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['retry-after'], 1000) | ||
t.deepEqual({ | ||
statusCode: 429, | ||
error: 'Too Many Requests', | ||
message: 'Rate limit exceeded, retry in 1 second' | ||
}, JSON.parse(res.payload)) | ||
setTimeout(retry, 1100) | ||
}) | ||
}) | ||
}) | ||
function retry () { | ||
fastify.inject('/', (err, res) => { | ||
redis.flushall(noop) | ||
redis.quit(noop) | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.headers['x-ratelimit-limit'], 2) | ||
t.strictEqual(res.headers['x-ratelimit-remaining'], 1) | ||
}) | ||
} | ||
}) | ||
test('Skip on redis error', t => { | ||
t.plan(13) | ||
const fastify = Fastify() | ||
const redis = new Redis({ host: '127.0.0.1' }) | ||
fastify.register(rateLimit, { | ||
max: 2, | ||
timeWindow: 1000, | ||
redis: redis, | ||
skipOnError: true | ||
}) | ||
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) | ||
redis.flushall(noop) | ||
redis.quit(err => { | ||
t.error(err) | ||
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'], 2) | ||
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'], 2) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
14038
8
292
68
4
Updatedfast-json-stringify@^1.2.0
Updatedtiny-lru@^1.5.2