Skip to content
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

Is Connexion 3 Compatible with Flask-Limiter? Recommendations for Flask rate-limiting with Connexion 3.0? #1942

Open
Parthib opened this issue Jun 21, 2024 · 2 comments

Comments

@Parthib
Copy link

Parthib commented Jun 21, 2024

Background

Flask-Limiter is a popular tool used to rate-limit endpoints of Flask applications.

We currently use it on our Flask server using connexion 2.14.2. However, due to the ASGI nature of Connexion 3.0, we are facing issues with the extension.

A basic use case of Flask-Limiter would be:

from flask import Flask

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)
limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["200 per day", "50 per hour"],
    storage_uri="memory://",
)


@app.route("/slow")
@limiter.limit("1 per day")
def slow():
    return ":("

Internally, Flask-Limiter uses flask.request.endpoint to retrieve the key it should use to rate-limit for, but I don't think flask.request is really accessible in connexion 3.0. Whenever I attempt to, I get an exception stating

This typically means that you attempted to use functionality that needed
an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.

Attempted Solution

As I understand from reading the migration docs, connexion requests are now Starlette requests that can be retrieved via from connexion import request, so I attempted to take advantage of this. Flask-Limiter allows you define a callable in the Flask config RATELIMIT_REQUEST_IDENTIFIER that replaces the use of flask.Request.endpoint, so I tried the following:

  1. Create a Middleware that adds the endpoint to the request scope:
class RateLimitMiddleware(BaseHTTPMiddleware):

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
        request.scope["endpoint"] = request.url.path
        response = await call_next(request)
        return response
        
...

connex_app = connexion.FlaskApp(
    __name__, specification_dir="openapi/_build", swagger_ui_options=swagger_ui_options
)

connex_app.add_api(
    "openapi.yaml",
    resolver=connexion.resolver.RestyResolver("app.api"),
    pythonic_params=True,
    validate_responses=True,
    strict_validation=True,
    uri_parser_class=connexion_mods.OpenAPIURIParserWithDictLists,
)

connex_app.add_middleware(RateLimitMiddleware, position=MiddlewarePosition.BEFORE_EXCEPTION)
  1. Access the underlying Flask app to set the config:
app: flask.Flask = connex_app.app
app.config.from_object(config[config_name])
  1. In my config, set RATELIMIT_REQUEST_IDENTIFIER to a function that retrieves the endpoint from the scope of the Starlette request:
from connexion import request

class BaseConfig:
    """Configures common variables and other settings for the backend."""
    def get_endpoint():
        return request.scope["endpoint"]

    RATELIMIT_REQUEST_IDENTIFIER = get_endpoint

Unfortunately, this never seems to be within the scope of a connexion request as I still get the same Runtime exception:

RuntimeError: Working outside of application context.

My Questions

  1. Connexion for Flask used to be compatible with several other Flask libraries like Flask-Limiter which uses the underlying Flask config, but utilizing the Flask config and other Flask context variables (flask.request, flask.g) no longer seems to be supported with Connexion 3.0. Is there an alternative way to use these extensions that I am overlooking?
  2. For Flask-based applications using Connexion 3.0, what do you recommend for performing API rate-limiting? I don't see any solutions other than some in-house solution that takes advantage of a custom Middleware. I took a look at slowapi which has more of a focus on Starlette requests, but that doesn't seem compatible with Connexion either since it requires endpoints to take in a Starlette requests object.
@whoseoyster
Copy link

+1 on this

@Parthib
Copy link
Author

Parthib commented Jun 21, 2024

Tried removing our rate limiting logic, and it looks to me that there is a bigger issue here:

Connexion's security handlers are now performed by middleware that exist outside of the Flask application, so it is not possible to access the Flask request context in the security handlers. Unfortunately for us, we have a dependency on Flask SQLAlchemy for our security handling that relies on access to the flask request context:

@decorators.setup_security_sentry_scope
def basic_auth(email: str, password: str, request):
    try:
        user: models.User = models.User.query.filter_by(email=email).one_or_none()
            ...

Stacktrace:

  File "/backend/lib/python3.9/site-packages/connexion/security.py", line 569, in verify_fn
    token_info = await token_info
  File "/backend/lib/python3.9/site-packages/connexion/security.py", line 116, in wrapper
    token_info = func(*args, **kwargs)
  File "/backend/app/api/decorators.py", line 59, in wrapper
    result = security_function(*args, **kwargs)
  File "/backend/app/api/connexion_auth.py", line 24, in basic_auth
    user: models.User = models.User.query.filter_by(email=email).one_or_none()
  File "/backend/lib/python3.9/site-packages/flask_sqlalchemy/model.py", line 23, in __get__
    cls, session=cls.__fsa__.session()  # type: ignore[arg-type]
  File "/backend/lib/python3.9/site-packages/sqlalchemy/orm/scoping.py", line 220, in __call__
    sess = self.registry()
  File "/backend/lib/python3.9/site-packages/sqlalchemy/util/_collections.py", line 632, in __call__
    key = self.scopefunc()
  File "/backend/lib/python3.9/site-packages/flask_sqlalchemy/session.py", line 111, in _app_ctx_id
    return id(app_ctx._get_current_object())  # type: ignore[attr-defined]
  File "/backend/lib/python3.9/site-packages/werkzeug/local.py", line 508, in _get_current_object
    raise RuntimeError(unbound_message) from None

There was a recent change to allow the security handling logic access to the ConnexionRequest request object, but that doesn't help us here because our dependency needs access to the flask application context.

@RobbeSneyders since you recently worked on passing the ConnexionRequest to the security handler - do you have any recommendations for our use case? Essentially we have dependencies in our security handling path that requires access to the flask application context, and this does not seem possible in Connexion 3.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants