Is Connexion 3 Compatible with Flask-Limiter? Recommendations for Flask rate-limiting with Connexion 3.0?
Parthib opened this issue · 2 comments
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:
- 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)
- Access the underlying Flask app to set the config:
app: flask.Flask = connex_app.app
app.config.from_object(config[config_name])
- 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
- 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?
- 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.
+1 on this
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