flask-openapi3 icon indicating copy to clipboard operation
flask-openapi3 copied to clipboard

Issues using flask-openapi3 when its views are decorated

Open maldoinc opened this issue 1 year ago • 1 comments

Environment:

  • Python version: 312
  • Operating system:
  • Flask version:
  • flask-openapi3 version:

This affects all versions. Flask-openapi3 removes all arguments passed to the view by any previous callers -- such as Dependency Injection frameworks, breaking things in the meanwhile.

In request._validate_request all arguments kwargs are removed as they are treated as path args. This is however not always a correct assumption as views from flask-openapi3 may be decorated again before being passed onto flask.

In this case flask calls the views decorated elsewhere which supply some arguments to flask-openapi3 which then simply removes them instead of passing them down. There are a few flask extensions which do this as well.

In the below example the call chain is as follows flask -> inject_services -> flask_openapi3.

Example:

@app.get("/greet")
def view(greeter: GreeterService, query: GreetQuery):
    return greeter.greet(query.name)

...

# Automatically decorate all flask views so that known services are passed down.
app.view_functions = {k: inject_services(v) for k, v in app.view_functions.items()}

...

maldoinc avatar Feb 13 '24 22:02 maldoinc

Not an entirely similar situation, but something that we run into.

We use decorators to provide authentication checks. A similar approach is flask-login.

@app.route("/settings")
@login_required
def settings():
    pass

Using this sort of set-up together with openapi3 means:

  1. flask handles incoming request (routing)
  2. flask-openapi3 checks incoming query-string, body, etc via the _validate_request (validation)
  3. decorators come into play (authorization)

Meaning our authentication decorator goes off AFTER the payload, query-string and all that has been checked. So we get validation before authorization! Which is in my opinion a bad pattern.

Personally, I think it's better to first check;

  1. Route existence
  2. Authorization
  3. Validation

I don't see a easy solution to this, since the current set-up with _validate_request is on a high level.

Perhaps move the _validate_request logic into a decorator? Then we control the flow by the order of decorators.

Then you could remove all the validation_error_callback logic and just document how to catch ValidationError in a errorhandler on app level. Now you're trying to do everything, which isn't needed IMO.

puittenbroek avatar Feb 03 '25 13:02 puittenbroek

@luolingchun This is quite a big issue we face using your project. The validation before authentication is very undesirable.

puittenbroek avatar May 06 '25 07:05 puittenbroek

I'm working on a PR with these ergonomics. What do you think? @maldoinc @puittenbroek

@api_view.route("/protected")
class ProtectedAPIView:
    decorators = [require_auth_decorator]

    @api_view.doc(summary="get protected resource")
    def get(self, query: BookQuery):
        return "protected data"

@api_view.route("/profile")
class ProfileAPIViewWithInjection:
    @api_view.doc(summary="get user profile", decorators=[inject_user_decorator])
    def get(self, query: BookQuery, user=None):
        """View receives user object injected by decorator"""
        return {"message": f"Profile for {user['name']}", "user_id": user["id"], "role": user["role"]}

@app.post("/books", summary="Create book", decorators=[inject_user_decorator])
def create_book(body: BookBody, user=None):
    return {"title": body.title, "created_by": user["name"], "user_id": user["id"]}

odie5533 avatar Nov 08 '25 18:11 odie5533

Hi @odie5533 check the documentation, in the meantime we've added a decorator which will delayed the validation.

https://github.com/luolingchun/flask-openapi3/blob/3e9b220d2f98867ab7da3a184aca24e99de96485/docs/Usage/Request.md#validate_request

This solves the issues for my team.

puittenbroek avatar Nov 11 '25 09:11 puittenbroek

No longer using this so I'll close, however I still stand by the original request(shown in the example above) in that this package should not blindly remove all passed arguments.

maldoinc avatar Nov 11 '25 11:11 maldoinc