alisaifee / flask-limiter

Rate Limiting extension for Flask

Home Page:https://flask-limiter.readthedocs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Callback to the first limit and resetting

TelegramXPlus opened this issue · comments

Hi, I'm using your superb extension which is helping me a lot with my project, but now I'm stuck with a problem. I need a function to be called only if a user is being limited, hence it should be called only once.

Then, how do I reset a specific user knowing their address (I'm using get_remote_address)? is a database going to help me? Something like extra dependencies pointed out in the documentation?

There's a recent addition to the extension - you can provide an on_breach callback to the limiter that will be triggered for any rate limit that is exceeded. The callback can be registered on the extension itself as follows:

limiter = Limiter(key_func=get_remote_address, on_breach=my_on_breach_callback)

@limiter.limit("1/second")
@app.route("/")
def root():
    return ...

or per limit as follows:

limiter = Limiter(key_func=get_remote_address)

@limiter.limit("1/second", on_breach=my_on_breach_callback)
@app.route("/")
def root():
    return ...

(Keep in mind, that if you do both, the function will be called twice).

The callback will be passed a single parameter, a RequestLimit instance which will give you access to details of the rate limit that was breached.

This feature isn't very well documented so if it sounds like it might fit your needs, we can try to figure out how to carve a solution around it.

Tbh it fits perfectly with my dilemma, thus thanks very much for the fast response.

Just another quick question: how do I free a user knowing their address? I'd like to create a route where I put the IP to give access again to a specific user (IP)

Thanks again for the fast answer and the beautiful extension you are granting us

Thank you for the kind words 🤗 - I'm always surprised and happy that this extension has managed to last this long and that it continues to be useful.

Regarding resetting rate limits - it is definitely possible since the underlying limits library exposes an api to clear an individual limit reference. The way the full "key" for a specific instance of the limit is constructed is broadly {key_prefix}/{return_from_key_function}/{name_of_route}

The problem that has often come up is knowing "all" the different keys that you'd have to clear if your application has rate limits applied to many routes (this becomes a bit more complicated when you throw blueprints, and route specific limits etc into the mix).

With an example setup like this:

limiter = Limiter(key_func=my_key_func, default_limits = ["10/second", "10000/day"], application_limits=["1000/second"])
limiter.init_app(limiter)

@app.route("/")
def r1():
    return "42"

@app.route("/sub")
@limiter.limit("50/second", key_func=my_other_key_func)
def s1():
   return "41"

.... bunch of other routes

In the above example, every route will have two keys per unique user "key" returned by my_key_func that will look like LIMITER/{return_from_my_key_func}/r1/10/second, LIMITER/{return_from_my_key_func}/r1/10000/day and for s1 it'll be just LIMITER/{return_from_my_other_key_func}/s1/50/second (since it provides a separate key_func for whatever reason). Additionally there will be one key per unique user for the application limits which will look like LIMITER/{return_from_my_key_func}/global/1000/second.

If you had added the key_prefix parameter when initializing the Limiter, each key would instead start with LIMITER/{key_prefix}/....

As you can see this quickly gets out of hand in terms of 'clearing all limits for a specific user'.

If it's a specific route you want to clear the user for and you have a specific set of rate limits that you know are applied to that route you can do something like this:

from limits import parse_many

limit_strings = ["10/second", "100000/day", ....]
items = [parse(limit_string) for limit_string in limit_strings]
keys = [item.key_for(return_from_key_func, name_of_route)]

# where limiter is your Flask-Limiter instance
[limiter.storage.clear(key) for key in keys]

name_of_route in most cases will just be the name of the view function you used (you can inspect all the view functions by inspecting app.view_functions where app is your flask application.

Thanks again for your kindness but I think I didn't understand well how the clearing of a user works. In my case, I just have a simple flask application using the factory pattern. I only have one limiter that uses the same key_func, hence it shouldn't be that deep. My problem comes across when I try to clear the limit for instance myself:

limiter = Limiter(key_func=get_remote_address)
@app.route('/someroute'):
    limiter._storage.clear(get_remote_address())
    # supposing I got limited. Also I had to use the protected member cause I couldn't find storage property

Yet this code does not work, because I couldn't see the list of limited IPs I wasn't even able to check the list of the storage; I only know the value returned from get_remote_address.

Thanks again in advance

My mistake in the code snippet - it should be limiter.limiter.storage() or as you discovered limiter._storage (In a follow up I'll probably make that attribute public).

Here's a working example that might make it easier to find a solution that works for you.

from flask import Flask
from limits import parse_many

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)
limiter = Limiter(key_func=get_remote_address)

limiter.init_app(app)


@app.route("/")
@limiter.limit("1/second; 10/hour")
def root():
    # you can always access the "current limits for a route"  within the context of a request
    print([k.key for k in limiter.current_limits])
    # this above `current_limits` is equivalent to the snippet below. You would do something like this
    # if you want to do something outside of a request, for instance on the command line and you could
    # replace get_remote_address() with the actual IP you are concerned with 
    print(
        [
            l.key_for(get_remote_address(), "root")
            for l in parse_many("1/second; 10/hour")
        ]
    )
    # this will clear the both limits 
    [limiter.limiter.storage().clear(k.key) for k in limiter.current_limits]
    # or you could do it this way
    [limiter.limiter.storage().clear(l.key_for(get_remote_address(), "root")) for k in limiter.current_limits]


    return "42"


app.run()

Hope that ^ makes it a bit clearer?

It makes it way clearer to understand, so thanks but still I couldn't get rid of my problem. By trying your example, the interpreter says

File "C:\Users\lomon\OneDrive\Desktop\random\room_\env\lib\site-packages\flask\app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
File "C:\Users\lomon\OneDrive\Desktop\random\room_\env\lib\site-packages\flask\app.py", line 1518, in full_dispatch_request
    rv = self.handle_user_exception(e)
File "C:\Users\lomon\OneDrive\Desktop\random\room_\env\lib\site-packages\flask\app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
File "C:\Users\lomon\OneDrive\Desktop\random\room_\env\lib\site-packages\flask\app.py", line 1502, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
File "C:\Users\lomon\OneDrive\Desktop\random\room_\env\lib\site-packages\flask_limiter\extension.py", line 1060, in __inner
    R, flask.current_app.ensure_sync(cast(Callable[P, R], obj))(*a, **k)
File "C:\Users\lomon\OneDrive\Desktop\random\room_\testFl.py", line 28, in root
    [limiter.limiter.storage().clear(k.key) for k in limiter.current_limits]
File "C:\Users\lomon\OneDrive\Desktop\random\room_\testFl.py", line 28, in <listcomp>
    [limiter.limiter.storage().clear(k.key) for k in limiter.current_limits]
TypeError: 'weakproxy' object is not callable

Regarding my project instead, I need to clear the limits of a route in another root, but when I print the lists of current_limits it gives me an empty list due to the fact of executing this code in a route not marked as limited.

Sorry for bothering you again, but really I don't understand where all these troubles are coming from.
Thanks again for your patience.

Ouch, sorry - the limiter.storage() example was based off an older version of limits - which I just updated. It should be:

limiter.limiter.storage.clear(...)

regarding your specific usecase, if you need to clear the limits of one route from another it would look like this:

@app.route("/route1")
@limiter.limit("1/minute")
def route1():
    return "..."


@app.route("/route2")
def route2():
    str(limiter.limiter.storage.clear(
        parse("1/minute").key_for(get_remote_address(), "route1")
    ))
    return "cleared"

Thanks for your given solution. Really explicative and it works great. Again thanks very much for the help, keep it up for the extension because it's sublime.