vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.24k stars 430 forks source link

Support for middleware #976

Open jellis18 opened 11 months ago

jellis18 commented 11 months ago

Is your feature request related to a problem? Please describe.

I know that django-ninja is based off of django, obviously, but I'm really not a fan of the way django handles middleware where it applies globally and is somewhat hard to configure since you have to pass in the path to a callable as a string instead of being able to define it in code.

To me it is always a pain (not just ninja, but django in general) when I want certain middleware per route or even per specific endpoint. I know we can do it with decorators or by subclassing router and using view_decorator now but all of those seem a bit tedious.

It seems that django-ninja is missing a key middleware feature like in FastAPI add_middleware and express with use or something like go Chi use as well. In all of those systems you can add middleware directly from the router without needing to use a decorator or equivalent.

Describe the solution you'd like I've already started prototyping this and its somewhat based on how go Chi does it. Basically what I am proposing is that middleware be defined like

api = NinjaAPI()

api.use(some_middleware)
# or
api.use(some_middleware1, some_middleware2)

This would add global middleware. To add router based middleware would look like

router = Router()
router.use(some_router_middleware)
# or
router.use(some_router_middleware1, some_router_middleware2)

In this case the middleware from api would be inherited and the router would then have its own that comes after the api middleware

We could go further and even have endpoint level middleware that could look like

router = Router()
...

@router.with(some_endpoint_middeware).get(...)
def some_endpoint_handler(request: HttpRequest):
   ...

In this case the middleware stack would include any from the global api, the router, and then this specific endpoint based middleware.

In all cases the middleware would have a signature that obeys

_P = ParamSpec("_P")
ViewFunction = Callable[Concatenate[HttpRequest, _P], HttpResponse]

MiddlewareFunction = Callable[[ViewFunction], ViewFunction]

Which all standard django decorators would already obey.

Again, I know there are ways to accomplish the same goal already but this could be really useful and would allow users to more easily add their own middlware without having to subclass router or add decorators everywhere.

What are your thoughts on this?. I'd be happy to work on this as I've already started but just want to see what the community thinks.

jkgenser commented 11 months ago

I like this very much. I also come from FastAPI so maybe I'm biased

jellis18 commented 11 months ago

Digging in a bit more into how django does middleware and while this would work, and I still think it is worthwhile, its not the same "middleware" that Django uses. The Django middleware stack gets applied inside of WSGIHander or ASGIHandler and ninja has no access to that as ninja gets requests after the middleware has already run and the view has already been resolved (i.e. it has args and kwargs injected into the view depending on the path).

Maybe this just boils down to a semantic problem and maybe we can call it something other than middleware but really it is middlware just not "django middleware"

vitalik commented 11 months ago

@jellis18 @jkgenser

where do you think execution of these middlewares should happen ?

I think 1st is basically covered with standard django middleware

as for 2nd case - basically it's a decorator - so yeah.. maybe router.use(decorator) can automatically added to all functions

maybe even with some mode

router.use(decorator, mode="before") # will run before input validation
router.use(decorator) # default will run after data is validated

I guess we need first to define some use cases and then come with some solution...

jellis18 commented 11 months ago

@jellis18 @jkgenser

where do you think execution of these middlewares should happen ?

  • before input validation (basically accessing only to request/response objects)
  • after input validation (having access to request and all the validated kwargs)

I think 1st is basically covered with standard django middleware

as for 2nd case - basically it's a decorator - so yeah.. maybe router.use(decorator) can automatically added to all functions

maybe even with some mode

router.use(decorator, mode="before") # will run before input validation
router.use(decorator) # default will run after data is validated

I guess we need first to define some use cases and then come with some solution...

Overall though I think they can act like decorators on the outside of the @api.method decorator so the signature is at least somewhat constant (i.e. Callable[Concatenate[HttpRequest, _P], HttpResponse])

Some of the main use cases that led me to thinking about this were permissions which would more easily be applied per router instead of API wide or as decorators on every different. Another thing that would be useful is setting context from the request in order to create contextual loggers. But there are lots of use cases where router based or per-route middleware/decorators would be useful.

kosuke-zhang commented 11 months ago

If there is middleware, I can easily set request.user #990

stvdrsch commented 9 months ago

@jellis18 @jkgenser

where do you think execution of these middlewares should happen ?

  • before input validation (basically accessing only to request/response objects)
  • after input validation (having access to request and all the validated kwargs)

I think 1st is basically covered with standard django middleware

as for 2nd case - basically it's a decorator - so yeah.. maybe router.use(decorator) can automatically added to all functions

maybe even with some mode

router.use(decorator, mode="before") # will run before input validation
router.use(decorator) # default will run after data is validated

I guess we need first to define some use cases and then come with some solution...

I just want to add that option 1 isn't fully covered by django middleware as that will also apply to requests for accessing the docs. Ofcourse it's possible to work around it.

torx-cz commented 4 months ago

Hello, I'd like to ask for your opinion.

I need to do a header check (if the version is correct), but it needs to be before the input schema starts validating.

This would be fairly easy using Django-middleware however, I only need to do this on certain endpoints.

The problem I'm having is that as soon as I use the decorator, a validation error (422) is thrown before my custom check, which I don't want.

Example decorator:

router = Router()
...

@router.get(...)
@my_header_checker
def some_endpoint_handler(request: HttpRequest, data: SomeSchema)
...

I want to ask if there is any way to run some checks before input validation.

Thank you for your time and reply.