laurentS/slowapi

Setting two different rate limits, does not work

alexjolig opened this issue · 4 comments

I'm testing this sample code to check the library:

import uvicorn
from fastapi import FastAPI, Request, Response, status
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)


# Note: the route decorator must be above the limit decorator, not below it
@app.get("/")
@limiter.limit("2/minute")
async def homepage(request: Request):
    return JSONResponse(
        status_code=status.HTTP_200_OK, content={"message": "Hello There!"}
    )


@app.get("/mars")
@limiter.limit("3/minute")
async def homepage(request: Request, response: Response):
    return JSONResponse(
        status_code=status.HTTP_200_OK, content={"message": "You only have 3 request per second"}
    )


if __name__ == '__main__':
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

But here's the problem, This endpoint works well for limiting only 2 request per minute:

 @app.get("/")
@limiter.limit("2/minute")
async def homepage(request: Request):

But this one does not work for 3 requests per minute and throws error after requesting 2 times:

@app.get("/mars")
@limiter.limit("3/minute")
async def homepage(request: Request, response: Response):

I get this error, which seems unrelated to "3/minute"

{"error":"Rate limit exceeded: 2 per 1 minute"}

Am I missing something here?

@alexjolig Can you change the name of the functions so they aren't the same and try again? Both of your functions are named homepage and this can cause issues when FastAPI interprets the file.

@twcurrie You were right. I somehow missed that. changing the name of the second function fixed the issue. But I have to mention that I did the same mistake using limits library with a decorator I wrote and it worked fine:

import uvicorn
import asyncio
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from limits import parse

from redis.redis import coredis
from helpers.limit_helpers import handle_rate_limit

two_per_minute = parse("2/minute")
three_per_minute = parse("3/minute")

app = FastAPI()


# Note: the route decorator must be above the limit decorator, not below it
@app.get("/")
@handle_rate_limit(two_per_minute)
async def homepage(request: Request):
    return JSONResponse(
        status_code=status.HTTP_200_OK, content={"message": "Hello There!"}
    )


@app.get("/about")
@handle_rate_limit(three_per_minute)
async def homepage(request: Request):
    return JSONResponse(
        status_code=status.HTTP_200_OK,
        content={"message": "You can read all about us, but only 3 times per minute"},
    )


if __name__ == "__main__":
    asyncio.run(coredis())
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

@twcurrie You were right. I somehow missed that. changing the name of the second function fixed the issue. But I have to mention that I did the same mistake using limits library with a decorator I wrote and it worked fine:

Without the source code for your custom decorator, I can't comment on why it would work, but did you use functools.wraps with your custom decorator? By using that decorator, it preserves the original function name, hence the conflicts in methods interpretted by FastAPI that are decorated with slowapi library.

@twcurrie Yes, I used functools.wraps in my decorator:

def handle_rate_limit(limitation_item: str):
    """
    This is used as a decorator for API routers to implement rate limitation
    :param limitation_item: limitation value (e.g: "2/minute", "10/day")
    :return: response
    """

    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            if not await moving_window.hit(parse(limitation_item)):
                return JSONResponse(
                    status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                    content={"message": "Maximum requests per time exceeded!"},
                )
            return await func(*args, **kwargs)

        return wrapper

    return decorator