New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
updated plugin following best practice from issue#10. #33
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests in CI are not running due to the linting.
Please, fix them so we can check easily this PR.
In most of the work can be done running standard --fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very good work!
I would also like to retain the previous behavior. By default, the rate limit is applied to all routes. It can be disabled, and then it’s per-route. We should also support keeping it enabled for all routes, but off or with different settings for one.
store/LocalStore.js
Outdated
this.timers[keyName] = setTimeout(() => { | ||
this.lru.delete(keyName) | ||
this.timers[keyName] = null | ||
}, timeWindow) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would need an unref().
The previous approach was way more performant. This is going to create a significant higher amount of timers.. and those are expensive. Could this be refactored to avoid this and keep a global timer? I think keeping a plugin-wide timewindow make sense, otherwise things would become a bit more weird to debug.
I’m also ok on a per-route timewindow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @mcollina ,
Ok . I will change that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @mcollina ,
actually I'm not sure to understand, would you mind refactor this part. I added the unref
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very good work!
I would also like to retain the previous behavior. By default, the rate limit is applied to all routes. It can be disabled, and then it’s per-route. We should also support keeping it enabled for all routes, but off or with different settings for one.
done. I also updated the doc with what i've done today.
please validate.
Hello, A new version, I corrected There a 2 kind of whiteList :
The To use it, you need to set the option The good thing about this feature is that it allow the admin to inject new IP/username ( what ever was define with the I also added a new option in the definition of the plugin call I also refactor the plugin to add the support of the default behavior with the possibility to The doc was updated too with a example and the definition of each params. @mcollina regarding Local cache and your comment, feel free to change it the way you want. Thanks |
… generate the preHandler
… generate the preHandler
… generate the preHandler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think timeWindow
should just be a global settings. We are rate-limiting an ip, and the grace time should be shared by all routes, not by a single, specific route.
index.js
Outdated
cache: params.prefixCache ? params.prefixCache.replace(/[-,.;#*]/g, ':') : urlT | ||
} | ||
if (pluginComponent.isRedis && pluginComponent.whiteListInRedis) { | ||
prefix.wl = urlT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's urlT
? What's wl
? Please use long, descriptive variable names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const urlT = (routeOptions.url === '/') ? 'root' : routeOptions.url.replace(/\//g, ':').slice(1)
urlT
is the url transformed in case the user did not give a prefixCache
. It will also make sure that if it's /
( root ), it will replace it with a string (root
) as by default I replace the /
by :
.
wl
is for the whiteList
, it's a shortname for the variable.
When using redis
the key look like this by default :
**For the whiteList define in the plugin (global
) **
${pluginComponent.app}:wlg:${prefix.wl}
For the whiteList define in the route
using { config : rateLimit : { whitelist : [...] } }
${pluginComponent.app}:wle:${prefix.wl}
pluginComponent.app
is the name of the app define in the plugin
with the option appName
. It's useful for clarity in redis.
Let me know if further question.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use log variables, and add the above explanation as a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, Let me comment the whole code if you want. it will be easier to review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @mcollina , commented all the code. Should be better to read.
store.incr(key, onIncr) | ||
if (pluginComponent.whitelist.global.indexOf(key) > -1 || pluginComponent.whitelist.endpoint[prefix.cache].indexOf(key) > -1) { | ||
next() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this logic. This PR has already too much change, and it's getting hard to review. Would you mind removing the new whitelist mechanism and send it as a follow-up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok.
res.header('X-RateLimit-Limit', params.max) | ||
res.header('X-RateLimit-Remaining', params.max - current) | ||
|
||
if (typeof params.onExceeding === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to pass in a nop
implementation and avoid the if check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I don't know nop
. I will check, you mean the if ...
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function nop () {}
, stick it at the end of the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I will check :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry I look into it but I might be stupid I'm not sure how to do what you request with the nop
function, feel free to modify as you wish it to be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you validate the settings:
params.onExceeding = params.onExceeding || nop
Then you can avoid the if-check completely. V8 will optimize the function call away.
index.js
Outdated
} | ||
} else { | ||
if (routeOptions.config && routeOptions.config.rateLimit && typeof routeOptions.config.rateLimit === 'object') { | ||
buildRouteRate(pluginComponent, makeParams(routeOptions.config.rateLimit), routeOptions, next) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not pass next
here. It's the outer next, and you have already called it when this happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I had a doubt about that, I will change that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it could work to return a new plugin
from within the rate-limit-plugin
, like a plugin generated on the fly ?
What I mean is that I could do something like :
const fp = require('fastify-plugin')
............................
.... some code ....
............................
return fp(buildRouteRate(fastify, {
components : pluginComponent
params : , makeParams(routeOptions.config.rateLimit),
route : routeOptions
}, next))
instead of :
buildRouteRate(pluginComponent, makeParams(routeOptions.config.rateLimit), routeOptions)
Would it be suitable, or it's not really in the logic of the fp
package ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It won't work. But I don't understand why you'll need this. Also, why are you passing next
there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the next
in buildRouteRate
is a mistake on my side, as you said earlier it's not needed .
I will commit the update at lunch, Sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated.
Co-Authored-By: matth-c3 <30625765+matth-c3@users.noreply.github.com>
README.md
Outdated
onExceeding: function (req) { | ||
console.log('callback on exceededing ... executed before response to client') | ||
}, | ||
>>>>>>> 6c3daf23c35af4d1d62ff685699f2e466386424a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes sorry .. problem during the merge ...
index.js
Outdated
const max = opts.max || 1000 | ||
const whitelist = opts.whitelist || [] | ||
const after = ms(timeWindow, { long: true }) | ||
// const makeParams = (p) => { return { ...globalParams, ...p } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this ommented out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was crashing during the test on a older version of node.
test.js
Outdated
// t.strictEqual(res.headers['x-ratelimit-limit'], undefined) | ||
// t.strictEqual(res.headers['x-ratelimit-remaining'], undefined) | ||
// }) | ||
// }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this commented out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test was not going throw, I prefer us to review the code first if that's fine with you as all 99 previous test goes throw.
New test need to be created anyway, I will fix this one when the time will come.
I'd still prefer to have the code for the new whitelist in its own PR. This is already big as a contribution. Also, I think the |
Well from my point of view it's more like a refresh / renew than a contribution. I'm not greedy or lazy but the code is clear enough, same as the comment. If it's not just let me know and I will simply start a different plugin on my own. Please be aware that the whitelist in redis is just a start, I already have a working version where params are adapting/updating dynamically from redis.
Sure as mention already feel free to change it and I will pull your changes later on today. Thanks. |
This is not the right attitude to take when submitting a PR. It is easiest to get changes accepted when 1. the PR is limited to one overall change and 2. you listen to the reviewers's feedback. This PR started out as one thing and then half-way through it you added in more:
This is after your work on the original intent of the PR was commended:
The reviewers are simply trying to help you get the PR into a mergeable state. Big features can take a few days of review back-and-forth to get right. But that whole back-and-forth process is how good, maintainable, code gets included in an OSS project. |
@jsumners in that case take the time that you ( reviewers ) needs to take to do instead of dictating on what I should PR or not. If you need a week to valid those changes than take the week, even two or three but don't comment the way you (yes you) do because it's not helping. So far I complied and did what I was asked to do/change/redo ( maybe even more ). If you ( reviewers ) want something to be change, you can do it and we (contributors) would be grateful too as it's how collaborative team work and things are moving. Again, if you are not satisfy, you think that it's too much or you that's simply too complicated, just close the PR. Like yours, my time is precious too. Think about that all of that next time before you answer to someone. |
You are probably saying this to one of most open teams in the land of Node.js: this is a friendly and open community. In fact, none of what we said was disrespectful in any form. The truth is we care a lot about maintenance. Keeping some code around that has some relevant issues is going to cause problems in the future. Landing a change we are not convinced off would mean that we have to rework that PR, or fix a lot of bugs on it in the future. Once this is landed, you might walk away or stay around - I cannot rely on it. As such, I need to minimize that will take to future us to maintain this. Note that this is an amazing feature and really good work done so far. However, I would like to land this with the minimum diff, as it's almost a rewrite. I'll push the changes on the |
@matth-c3 can you enable me to push to your repo? It's blocked and I can't push. |
I fix my review comments about |
Ok, just let me know when you merge your change in your repo that I can delete mine. Thanks. |
Hello @mcollina , is there any action remaining on my side ? |
I would like to use |
Hello @mcollina, on my side I will not use it, just change what you feel like :) maybe it's better for you to create a new branch instead of working on the fork ? |
closing in favor of #34 |
The plugin has been reviewed, there is a example folder with an example of usage, I did not prepare the test file yet but the example show all the cases possible.
Let me know if that's suitable for you.
Here how it works, with
fastify
we register theplugin
only once with thedefault
parameter value that we would apply if this one is missing in the rules ( or just not set ).The main options are :
max
: set the max fetch that can do a user during a definite period of time;timeWindow
: to set the time framecache
: not use I thinkwhitelist
: is an array of ip which will not be restrictedredis
: connection redis ( from pool for example )skipOnError
: skip errors;keyGenerator
: to generate a different key to identify the user in the cache. By default it's the ip, but It could also be the username. To adapt based on the infra, in case of reverse proxy for example.example of
get
with all the parameter and the 2 callback :onExceeding: when the user is currently fetching/consuming his rate limit
onExceeded : when the user reached his rate limit.
an other example. Here we can see that
max
was not provided, so it will automatically get the default value set in the plugin definition/registration withfastify
. In this case it will be3000
You will notice that whitelist is in the
main config
of the plugin and also in therule
. There is actually 2 different scope.One scope global : define in the
main conf
. Will be use if you trust the source to access everything without limit;One scope endpoint : define in the individual
rule
, to allow a source to access only specific endpoint.Let me know what you think about this.
Thanks.