fastify-rate-limit
Advanced tools
Comparing version 5.0.1 to 5.1.0
@@ -31,3 +31,3 @@ /// <reference types="node" /> | ||
interface AddHeaders { | ||
interface DefaultAddHeaders { | ||
'x-ratelimit-limit'?: boolean, | ||
@@ -39,2 +39,9 @@ 'x-ratelimit-remaining'?: boolean, | ||
interface DraftSpecAddHeaders { | ||
'ratelimit-limit'?: boolean, | ||
'ratelimit-remaining'?: boolean, | ||
'ratelimit-reset'?: boolean, | ||
'retry-after'?: boolean | ||
} | ||
export interface RateLimitPluginOptions { | ||
@@ -56,3 +63,4 @@ global?: boolean; | ||
errorResponseBuilder?: (req: FastifyRequest, context: errorResponseBuilderContext) => object; | ||
addHeaders?: AddHeaders; | ||
addHeaders?: DefaultAddHeaders | DraftSpecAddHeaders; | ||
enableDraftSpec?: boolean; | ||
} | ||
@@ -59,0 +67,0 @@ |
48
index.js
@@ -11,2 +11,16 @@ 'use strict' | ||
let labels = { | ||
rateLimit: 'x-ratelimit-limit', | ||
rateRemaining: 'x-ratelimit-remaining', | ||
rateReset: 'x-ratelimit-reset', | ||
retryAfter: 'retry-after' | ||
} | ||
const draftSpecHeaders = { | ||
rateLimit: 'ratelimit-limit', | ||
rateRemaining: 'ratelimit-remaining', | ||
rateReset: 'ratelimit-reset', | ||
retryAfter: 'retry-after' | ||
} | ||
function rateLimitPlugin (fastify, settings, next) { | ||
@@ -19,9 +33,16 @@ // create the object that will hold the "main" settings that can be shared during the build | ||
if (typeof settings.enableDraftSpec === 'boolean' && settings.enableDraftSpec) { | ||
globalParams.enableDraftSpec = true | ||
labels = draftSpecHeaders | ||
} | ||
globalParams.addHeaders = Object.assign({ | ||
'x-ratelimit-limit': true, | ||
'x-ratelimit-remaining': true, | ||
'x-ratelimit-reset': true, | ||
'retry-after': true | ||
[labels.rateLimit]: true, | ||
[labels.rateRemaining]: true, | ||
[labels.rateReset]: true, | ||
[labels.retryAfter]: true | ||
}, settings.addHeaders) | ||
globalParams.labels = labels | ||
// define the global maximum of request allowed | ||
@@ -149,6 +170,8 @@ globalParams.max = (typeof settings.max === 'number' || typeof settings.max === 'function') | ||
const maximum = getMax() | ||
const timeLeft = Math.floor(ttl / 1000) | ||
if (current <= maximum) { | ||
res.header('x-ratelimit-limit', maximum) | ||
.header('x-ratelimit-remaining', maximum - current) | ||
.header('x-ratelimit-reset', Math.floor(ttl / 1000)) | ||
res.header(params.labels.rateLimit, maximum) | ||
.header(params.labels.rateRemaining, maximum - current) | ||
.header(params.labels.rateReset, timeLeft) | ||
@@ -165,6 +188,9 @@ if (typeof params.onExceeding === 'function') { | ||
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) } | ||
if (params.addHeaders[params.labels.rateLimit]) { res.header(params.labels.rateLimit, maximum) } | ||
if (params.addHeaders[params.labels.rateRemaining]) { res.header(params.labels.rateRemaining, 0) } | ||
if (params.addHeaders[params.labels.rateReset]) { res.header(params.labels.rateReset, timeLeft) } | ||
if (params.addHeaders[params.labels.retryAfter]) { | ||
const resetAfterTime = (params.enableDraftSpec) ? timeLeft : params.timeWindow | ||
res.header(params.labels.retryAfter, resetAfterTime) | ||
} | ||
@@ -171,0 +197,0 @@ const code = params.ban && current - maximum > params.ban ? 403 : 429 |
{ | ||
"name": "fastify-rate-limit", | ||
"version": "5.0.1", | ||
"version": "5.1.0", | ||
"description": "A low overhead rate limiter for your routes", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -79,2 +79,3 @@ # fastify-rate-limit | ||
errorResponseBuilder: function(req, context) { /* ... */}, | ||
enableDraftSpec: true, // default false. Uses IEFT draft header standard | ||
addHeaders: { // default show all the response headers when rate limit is reached | ||
@@ -102,2 +103,3 @@ 'x-ratelimit-limit': true, | ||
- `addHeaders`: define which headers should be added in the response when the limit is reached. Defaults all the headers will be shown | ||
- `enableDraftSpec`: if `true` it will change the HTTP rate limit headers following the IEFT draft document. More information at [draft-ietf-httpapi-ratelimit-headers.md](https://github.com/ietf-wg-httpapi/ratelimit-headers/blob/f6a7bc7560a776ea96d800cf5ed3752d6d397b06/draft-ietf-httpapi-ratelimit-headers.md). | ||
@@ -278,2 +280,15 @@ `keyGenerator` example usage: | ||
### IETF Draft Spec Headers | ||
The response will have the following headers if `enableDraftSpec` is `true`: | ||
| Header | Description | | ||
|--------|-------------| | ||
|`ratelimit-limit` | how many request the client can do | ||
|`ratelimit-remaining` | how many request remain to the client in the timewindow | ||
|`ratelimit-reset` | how many seconds must pass before the rate limit resets | ||
|`retry-after` | contains the same value in time as `ratelimit-reset` | ||
<a name="license"></a> | ||
@@ -280,0 +295,0 @@ ## License |
@@ -663,1 +663,137 @@ 'use strict' | ||
}) | ||
test('With enabled IETF Draft Spec', t => { | ||
t.plan(19) | ||
const fastify = Fastify() | ||
fastify.register(rateLimit, { | ||
max: 2, | ||
timeWindow: '1s', | ||
enableDraftSpec: true | ||
}) | ||
fastify.get('/', (req, reply) => { | ||
reply.send('hello!') | ||
}) | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.headers['ratelimit-limit'], 2) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 1) | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.headers['ratelimit-limit'], 2) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 0) | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 429) | ||
t.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8') | ||
t.strictEqual(res.headers['ratelimit-limit'], 2) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 0) | ||
t.strictEqual(res.headers['ratelimit-remaining'], res.headers['retry-after']) | ||
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) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.headers['ratelimit-limit'], 2) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 1) | ||
}) | ||
} | ||
}) | ||
test('hide IETF draft spec headers', t => { | ||
t.plan(17) | ||
const fastify = Fastify() | ||
fastify.register(rateLimit, { | ||
max: 1, | ||
timeWindow: 1000, | ||
enableDraftSpec: true, | ||
addHeaders: { | ||
'ratelimit-limit': false, | ||
'ratelimit-remaining': false, | ||
'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['ratelimit-limit'], 1) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 0) | ||
t.strictEqual(res.headers['ratelimit-reset'], 1) | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 429) | ||
t.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8') | ||
t.notOk(res.headers['ratelimit-limit'], 'the header must be missing') | ||
t.notOk(res.headers['ratelimit-remaining'], 'the header must be missing') | ||
t.notOk(res.headers['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['ratelimit-limit'], 1) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 0) | ||
t.strictEqual(res.headers['ratelimit-reset'], 1) | ||
}) | ||
} | ||
}) | ||
test('afterReset and Rate Limit remain the same when enableDraftSpec is enabled', t => { | ||
t.plan(16) | ||
const fastify = Fastify() | ||
fastify.register(rateLimit, { | ||
max: 1, | ||
timeWindow: '10s', | ||
enableDraftSpec: true | ||
}) | ||
fastify.get('/', (req, reply) => { | ||
reply.send('hello!') | ||
}) | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.headers['ratelimit-limit'], 1) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 0) | ||
setTimeout(retry.bind(null, 9), 500) | ||
setTimeout(retry.bind(null, 8), 1500) | ||
}) | ||
function retry (timeLeft) { | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 429) | ||
t.strictEqual(res.headers['ratelimit-limit'], 1) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 0) | ||
t.strictEqual(res.headers['ratelimit-reset'], timeLeft) | ||
t.strictEqual(res.headers['ratelimit-reset'], res.headers['retry-after']) | ||
}) | ||
} | ||
}) |
@@ -979,1 +979,24 @@ 'use strict' | ||
}) | ||
test('With enable IETF draft spec', t => { | ||
t.plan(5) | ||
const fastify = Fastify() | ||
fastify.register(rateLimit, { | ||
global: false, | ||
enableDraftSpec: true | ||
}) | ||
fastify.get('/', { | ||
config: defaultRouteConfig | ||
}, (req, reply) => { | ||
reply.send('hello!') | ||
}) | ||
fastify.inject('/', (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.headers['ratelimit-limit'], 2) | ||
t.strictEqual(res.headers['ratelimit-remaining'], 1) | ||
t.strictEqual(res.headers['ratelimit-reset'], 1) | ||
}) | ||
}) |
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
89145
2315
296
0