fastify-rate-limit
Advanced tools
Comparing version 3.0.1 to 4.0.0
@@ -74,5 +74,5 @@ 'use strict' | ||
KnexStore.prototype.child = function (routeOptions) { | ||
const options = Object.assign(this.options, routeOptions.config.rateLimit) | ||
const options = Object.assign(this.options, routeOptions) | ||
const store = new KnexStore(options) | ||
store.routeKey(routeOptions.method + routeOptions.url) | ||
store.routeKey(routeOptions.routeInfo.method + routeOptions.routeInfo.url) | ||
return store | ||
@@ -79,0 +79,0 @@ } |
'use strict' | ||
const Redis = require('ioredis') | ||
const redis = new Redis() | ||
const redis = new Redis({ | ||
connectionName: 'my-connection-name', | ||
host: 'localhost', | ||
port: 6379, | ||
connectTimeout: 500, | ||
maxRetriesPerRequest: 1 | ||
}) | ||
@@ -6,0 +12,0 @@ const fastify = require('fastify')() |
/// <reference types="node" /> | ||
import * as fastify from 'fastify'; | ||
import * as https from "https"; | ||
import { Server, IncomingMessage, ServerResponse } from "http"; | ||
import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse } from "http2"; | ||
import { FastifyPlugin, FastifyRequest, RouteOptions, RawServerBase, RawServerDefault, RawRequestDefaultExpression, RequestGenericInterface } from 'fastify'; | ||
type HttpServer = Server | Http2Server | Http2SecureServer | https.Server; | ||
type HttpRequest = IncomingMessage | Http2ServerRequest; | ||
type HttpResponse = ServerResponse | Http2ServerResponse; | ||
declare module "fastify" { | ||
interface FastifyReply<HttpResponse> {} | ||
declare module 'fastify' { | ||
interface FastifyRequestInterface< | ||
RawServer extends RawServerBase = RawServerDefault, | ||
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>, | ||
RequestGeneric extends RequestGenericInterface = RequestGenericInterface | ||
> { | ||
ip: string | number | ||
} | ||
} | ||
declare function fastifyRateLimit(): fastify.Plugin< | ||
Server, | ||
IncomingMessage, | ||
ServerResponse, | ||
{ | ||
global?: boolean; | ||
max?: number | ((req: fastify.FastifyRequest<IncomingMessage>, key: string) => number); | ||
timeWindow?: number; | ||
cache?: number; | ||
store?: fastifyRateLimit.FastifyRateLimitStoreCtor; | ||
whitelist?: string[] | ((req: fastify.FastifyRequest<IncomingMessage>, key: string) => boolean); | ||
redis?: any; | ||
skipOnError?: boolean; | ||
ban?: number; | ||
keyGenerator?: (req: fastify.FastifyRequest<IncomingMessage>) => string | number; | ||
errorResponseBuilder?: (req: fastify.FastifyRequest<IncomingMessage>, context: fastifyRateLimit.errorResponseBuilderContext) => object; | ||
addHeaders?: fastifyRateLimit.AddHeaders; | ||
} | ||
>; | ||
export interface FastifyRateLimitOptions {} | ||
declare namespace fastifyRateLimit { | ||
interface FastifyRateLimitOptions {} | ||
export interface errorResponseBuilderContext { | ||
after: string; | ||
max: number; | ||
} | ||
interface errorResponseBuilderContext { | ||
after: string; | ||
max: number; | ||
} | ||
export interface FastifyRateLimitStoreCtor { | ||
new (options: FastifyRateLimitOptions): FastifyRateLimitStore; | ||
} | ||
interface FastifyRateLimitStoreCtor { | ||
new (options: FastifyRateLimitOptions): FastifyRateLimitStore; | ||
} | ||
export interface FastifyRateLimitStore { | ||
incr(key: string, callback: ( error: Error|null, result?: { current: number, ttl: number } ) => void): void; | ||
child(routeOptions: RouteOptions & { path: string, prefix: string }): FastifyRateLimitStore; | ||
} | ||
interface FastifyRateLimitStore { | ||
incr(key: string, callback: ( error: Error|null, result?: { current: number, ttl: number } ) => void): void; | ||
child(routeOptions: fastify.RouteOptions<Server, IncomingMessage, ServerResponse> & { path: string, prefix: string }): FastifyRateLimitStore; | ||
} | ||
interface AddHeaders { | ||
'x-ratelimit-limit'?: boolean, | ||
'x-ratelimit-remaining'?: boolean, | ||
'x-ratelimit-reset'?: boolean, | ||
'retry-after'?: boolean | ||
} | ||
interface AddHeaders { | ||
'x-ratelimit-limit'?: boolean, | ||
'x-ratelimit-remaining'?: boolean, | ||
'x-ratelimit-reset'?: boolean, | ||
'retry-after'?: boolean | ||
} | ||
export interface RateLimitPluginOptions { | ||
global?: boolean; | ||
max?: number | ((req: FastifyRequest, key: string) => number); | ||
timeWindow?: number; | ||
cache?: number; | ||
store?: FastifyRateLimitStoreCtor; | ||
whitelist?: string[] | ((req: FastifyRequest, key: string) => boolean); | ||
redis?: any; | ||
skipOnError?: boolean; | ||
ban?: number; | ||
keyGenerator?: (req: FastifyRequest) => string | number; | ||
errorResponseBuilder?: (req: FastifyRequest, context: errorResponseBuilderContext) => object; | ||
addHeaders?: AddHeaders; | ||
} | ||
export = fastifyRateLimit; | ||
declare const fastifyRateLimit: FastifyPlugin<RateLimitPluginOptions>; | ||
export default fastifyRateLimit; |
@@ -66,3 +66,3 @@ 'use strict' | ||
? settings.keyGenerator | ||
: (req) => req.raw.ip | ||
: (req) => req.ip | ||
@@ -84,7 +84,4 @@ globalParams.errorResponseBuilder = (req, context) => ({ statusCode: context.statusCode, error: 'Too Many Requests', message: `Rate limit exceeded, retry in ${context.after}` }) | ||
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) | ||
mergedRateLimitParams.routeInfo = routeOptions | ||
current.store = pluginComponent.store.child(mergedRateLimitParams) | ||
// if the current endpoint have a custom rateLimit configuration ... | ||
@@ -91,0 +88,0 @@ buildRouteRate(current, mergedRateLimitParams, routeOptions) |
{ | ||
"name": "fastify-rate-limit", | ||
"version": "3.0.1", | ||
"version": "4.0.0", | ||
"description": "A low overhead rate limiter for your routes", | ||
@@ -9,3 +9,3 @@ "main": "index.js", | ||
"test": "standard && tap --cov test/*.test.js && npm run typescript", | ||
"typescript": "tsc --project ./test/types/tsconfig.json" | ||
"typescript": "tsd" | ||
}, | ||
@@ -28,23 +28,21 @@ "repository": { | ||
"devDependencies": { | ||
"@types/ioredis": "~4.0.11", | ||
"@types/node": "~12.11.0", | ||
"fastify": "^2.5.0", | ||
"@types/ioredis": "~4.16.0", | ||
"@types/node": "~13.13.4", | ||
"fastify": "^3.0.0-rc.1", | ||
"ioredis": "^4.9.0", | ||
"knex": "^0.20.2", | ||
"knex": "^0.21.1", | ||
"sqlite3": "^4.1.0", | ||
"standard": "^14.0.2", | ||
"tap": "^12.6.6", | ||
"typescript": "^3.4.2" | ||
"tap": "^14.10.7", | ||
"tsd": "^0.11.0" | ||
}, | ||
"dependencies": { | ||
"fast-json-stringify": "^1.14.0", | ||
"fastify-plugin": "^1.3.0", | ||
"fast-json-stringify": "^2.0.0", | ||
"fastify-plugin": "^2.0.0", | ||
"ms": "^2.1.1", | ||
"tiny-lru": "^7.0.0" | ||
}, | ||
"greenkeeper": { | ||
"ignore": [ | ||
"tap" | ||
] | ||
"tsd": { | ||
"directory": "test/types" | ||
} | ||
} |
# fastify-rate-limit | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/fastify/fastify-rate-limit.svg)](https://greenkeeper.io/) | ||
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) | ||
[![Build Status](https://travis-ci.org/fastify/fastify-rate-limit.svg?branch=master)](https://travis-ci.org/fastify/fastify-rate-limit) | ||
![CI workflow](https://github.com/fastify/fastify-rate-limit/workflows/CI%20workflow/badge.svg) | ||
@@ -87,3 +86,3 @@ A low overhead rate limiter for your routes. Supports Fastify `2.x` versions. | ||
- `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) | ||
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). **Note:**: the [default parameters](https://github.com/luin/ioredis/blob/v4.16.0/API.md#new_Redis_new) of a redis connection are not the fastest to provide a rate-limit. We suggest to customize the `connectTimeout` and `maxRetriesPerRequest` as in the [`example`](https://github.com/fastify/fastify-rate-limit/tree/master/example/example.js). | ||
- `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 | ||
@@ -145,2 +144,5 @@ - `skipOnError`: if `true` it will skip errors generated by the storage (eg, redis not reachable). | ||
Custom `store` example usage: | ||
NOTE: The ```timeWindow``` will always be passed as the numeric value in millseconds into the store's constructor. | ||
```js | ||
@@ -161,3 +163,3 @@ function CustomStore (options) { | ||
// route parameters and pass them into the child store. | ||
const childParams = Object.assign(this.options, routeOptions.config.rateLimit) | ||
const childParams = Object.assign(this.options, routeOptions) | ||
const store = new CustomStore(childParams) | ||
@@ -176,2 +178,6 @@ // Here is where you may want to do some custom calls on the store with the information | ||
The `routeOptions` object passed to the the `child` method of the store will contain the same options that are detailed above for plugin registration with any specific overrides provided on the route. In addition the following parameter is provided: | ||
- `routeInfo`: The configuration of the route including `method`, `url`, `path`, and the full route `config` | ||
### Options on the endpoint itself | ||
@@ -178,0 +184,0 @@ |
@@ -35,6 +35,6 @@ 'use strict' | ||
LocalStore.prototype.child = function (routeOptions) { | ||
return new LocalStore(routeOptions.config.rateLimit.timeWindow, | ||
routeOptions.config.rateLimit.cache, this.app) | ||
return new LocalStore(routeOptions.timeWindow, | ||
routeOptions.cache, this.app) | ||
} | ||
module.exports = LocalStore |
@@ -33,4 +33,4 @@ 'use strict' | ||
const child = Object.create(this) | ||
child.key = this.key + routeOptions.method + routeOptions.url + '-' | ||
child.timeWindow = routeOptions.config.rateLimit.timeWindow | ||
child.key = this.key + routeOptions.routeInfo.method + routeOptions.routeInfo.url + '-' | ||
child.timeWindow = routeOptions.timeWindow | ||
return child | ||
@@ -37,0 +37,0 @@ } |
@@ -783,2 +783,54 @@ 'use strict' | ||
test('timeWindow specified as a string', t => { | ||
t.plan(12) | ||
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() | ||
fastify.register(rateLimit, { | ||
global: false, | ||
store: CustomStore | ||
}) | ||
fastify.get('/', { | ||
config: { rateLimit: { max: 2, timeWindow: '10 seconds' } } | ||
}, (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) | ||
}) | ||
}) | ||
}) | ||
}) | ||
test('With CustomStore', t => { | ||
@@ -798,3 +850,3 @@ t.plan(18) | ||
CustomStore.prototype.child = function (routeOptions) { | ||
const store = new CustomStore(Object.assign(this.options, routeOptions.config.rateLimit)) | ||
const store = new CustomStore(Object.assign(this.options, routeOptions)) | ||
return store | ||
@@ -801,0 +853,0 @@ } |
import * as http from 'http' | ||
import * as http2 from 'http2' | ||
import * as fastify from 'fastify'; | ||
import * as types from '../../index'; | ||
import * as fastifyRateLimit from '../../../fastify-rate-limit'; | ||
import fastify, { RouteOptions, FastifyRequest, FastifyInstance } from 'fastify'; | ||
import * as ioredis from 'ioredis'; | ||
import fastifyRateLimit, { FastifyRateLimitStore, FastifyRateLimitOptions, errorResponseBuilderContext, RateLimitPluginOptions } from '../../'; | ||
class CustomStore implements types.FastifyRateLimitStore { | ||
constructor(options: types.FastifyRateLimitOptions) {} | ||
class CustomStore implements FastifyRateLimitStore { | ||
constructor(options: 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>{}) | ||
child(routeOptions: RouteOptions & { path: string, prefix: string }) { | ||
return <CustomStore>(<FastifyRateLimitOptions>{}) | ||
} | ||
@@ -17,3 +16,3 @@ } | ||
const appWithImplicitHttp = fastify() | ||
const options1 = { | ||
const options1: RateLimitPluginOptions = { | ||
global: true, | ||
@@ -27,4 +26,4 @@ max: 3, | ||
ban: 10, | ||
keyGenerator: (req: fastify.FastifyRequest<http.IncomingMessage>) => req.ip, | ||
errorResponseBuilder: (req: fastify.FastifyRequest<http.IncomingMessage>, context: fastifyRateLimit.errorResponseBuilderContext) => ({ code: 429, timeWindow: context.after, limit: context.max }), | ||
keyGenerator: (req: FastifyRequest<http.Server, http.IncomingMessage>) => req.ip, | ||
errorResponseBuilder: (req: FastifyRequest<http.Server, http.IncomingMessage>, context: errorResponseBuilderContext) => ({ code: 429, timeWindow: context.after, limit: context.max }), | ||
addHeaders: { | ||
@@ -40,4 +39,4 @@ 'x-ratelimit-limit': false, | ||
global: true, | ||
max: (req: fastify.FastifyRequest<http.IncomingMessage>, key: string) => (42), | ||
whitelist: (req: fastify.FastifyRequest<http.IncomingMessage>, key: string) => (false), | ||
max: (req: FastifyRequest<http.Server, http.IncomingMessage>, key: string) => (42), | ||
whitelist: (req: FastifyRequest<http.Server, http.IncomingMessage>, key: string) => (false), | ||
timeWindow: 5000 | ||
@@ -48,3 +47,3 @@ } | ||
global: true, | ||
max: (req: fastify.FastifyRequest<http.IncomingMessage>, key: string) => (42), | ||
max: (req: FastifyRequest<http.Server, http.IncomingMessage>, key: string) => (42), | ||
timeWindow: 5000, | ||
@@ -58,3 +57,3 @@ store: CustomStore | ||
const appWithHttp2: fastify.FastifyInstance< | ||
const appWithHttp2: FastifyInstance< | ||
http2.Http2Server, | ||
@@ -61,0 +60,0 @@ http2.Http2ServerRequest, |
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
74840
18
1907
265
+ Addedfast-json-stringify@2.7.13(transitive)
+ Addedfastify-plugin@2.3.4(transitive)
+ Addedrfdc@1.4.1(transitive)
+ Addedsemver@7.6.3(transitive)
- Removedfast-json-stringify@1.21.0(transitive)
- Removedfastify-plugin@1.6.1(transitive)
- Removedsemver@6.3.1(transitive)
Updatedfast-json-stringify@^2.0.0
Updatedfastify-plugin@^2.0.0