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

feat: add middleware to Router #1464

Closed
wants to merge 34 commits into from

Conversation

adriangb
Copy link
Member

@adriangb adriangb commented Feb 1, 2022

Closes #1286

Instead of adding middleware to each BaseRoute, we can add it to Router.

This can then be composed with Mount and such to get middleware for a single BaseRoute if needed, although this does make adding middleware to a single BaseRoute more complicated compared to #1286 since you may have to do a mount -> router -> route just to add middleware. But the most part I think users would be adding middleware to a group of routes not a single route, which works well with having it on Router.

One nice thing about this is that the behavior of middleware is preserved: you can even modify scope["path"] and change the outcome of routing.

Another advantage this has over #1286 is that it leaves the door open for us to move middleware from the Starlette app to Router, so we'd only have middleware in 1 class (I've tested this, it works nicely but requires some braking changes like removing Starlette.build_middleware_stack()).

@alex-oleshkevich what do you think?

@adriangb adriangb changed the title Router middleware redux feat: add middleware to Router Feb 1, 2022
@adriangb adriangb added the feature New feature or request label Feb 2, 2022
@adriangb
Copy link
Member Author

For what it's worth, I implemented this in Xpresso and it works very nicely. I can also confirm that, aside from breaking the deprecated decorators, moving all middleware to Router and out of App/Starlette would also work in the future, should we choose to take that path (this is what I did in Xpresso).

@Kludex
Copy link
Sponsor Member

Kludex commented Apr 26, 2022

@alex-oleshkevich Your opinion is highly appreciated here. :)

As we talked on Gitter some time ago, I was pretty skeptical about non-app-level-middlewares at the beginning, but as no one could find a better solution (myself included), I think this is a great addition. I also think this is simpler than #1286 .

In any case, I'm going to test this in the following weeks. Everybody, feel free to review this as well.

@gareth-leake
Copy link

Looking forward to seeing this addition!

@florimondmanca
Copy link
Member

florimondmanca commented May 20, 2022

I see that in #1286 Tom mentioned the possibility of passing middleware=... on Mount as well.

#1286 (comment)

Let's get the same thing added to Mount as well. Having middleware on mounts kinda makes more sense to middleware on routes to me.

On this PR, usage is as follows...

# apps/admin.py

router = Router(
    routes=[
        Route(
            "/dashboard,
            endpoint=...,
        )
    ],
    middleware=[Middleware(AdminAuthMiddleware, ...)],  # Sub-router-specific middleware
)

# app.py

from .apps import admin

routes = [
    Mount(
        "/admin",
        app=admin.router,
    )
]

app = Starlette(
    routes=routes,
    middleware=...,  # App-wide middleware
)

But what about Mount(..., routes=...)? Personally, I use this style more often, and it's also the one that's advertised in the docs: Sub-mounting routes.

It looks like only allowing Router(..., middleware=...) would make Mount(..., routes=...) sort of ill-defined: if one needs middleware on a sub-router, one would now always have to define a Router instance, and then pass it as app=....

Instead, with Mount(..., middleware=...), we'd have this...

# apps/admin.py

routes = [
    Route(
        "/dashboard,
        endpoint=...,
    ),
]

# app.py

from .apps import admin

routes = [
    Mount(
        "/admin",
        routes=admin.routes,
        middleware=[Middleware(AdminAuthMiddleware, ...)],  # Sub-router-specific middleware
    )
]

app = Starlette(
    routes=routes,
    middleware=...,  # App-wide middleware
)

This style makes the definition of sub-router middleware both closer to where app-level middleware is defined (generally the app routes are defined near Starlette), and just next to the mounting path, which is defined on Mount. This provides nice locality of behavior.

This would not only still work with Mount(..., app=Router(...), middleware=...), but also work for cases where app is an arbitrary ASGI app, not just a Starlette router. This embraces the composability principle of ASGI.

It also seems consistent if we think of Mount as a sub-app of Starlette (which it is), given that Starlette used to be the sole point for defining middleware=.... We'd just be expanding this capability to sub-apps.

Lastly, given all this, and while Tom initially thought "let's also add this to Mount", I wonder if we need Router(..., middleware=...) at all. So, given that sub-routes always involves Mount anyway, I might be in favor of keeping this style only: Mount(..., middleware=...).

@Kludex Kludex modified the milestones: Version 0.21.0, Version 0.22.0 May 22, 2022
@abersheeran
Copy link
Member

So……why not use this code?

AdminAuthMiddleware(Mount("/admin", routes=admin.routes), ...)

Just a question. I love routing middleware. But I don't think routing middleware is necessary in the Mount scenario.

@florimondmanca
Copy link
Member

@abersheeran You might be able to get that to work (does it?), but I suspect there might be issues related to root_path and other things that Mount() takes care of. Also, Mount(..., middleware=[A, B, ...]) would take care of applying multiple middleware in a reverse order, whereas you would need to write B(A(Mount(...), ...), ...).

@alex-oleshkevich
Copy link
Member

I vote for Mount solution. This looks solid and does exactly the same things as Mount already does -> attach another routes/app with extra settings (middleware in our case).

@abersheeran
Copy link
Member

@florimondmanca You convinced me. ^____^

@adriangb
Copy link
Member Author

Great points, thank you for bringing it up @florimondmanca

I'll cook up an implementation shortly (just rip it out of #1286).

@piyushere
Copy link

For those coming here from FastAPI,
you can achieve similar functionality using router dependencies as explained here:
tiangolo/fastapi#1174 (comment)

@HiperMaximus
Copy link

Are there any updates on this?

@adriangb
Copy link
Member Author

@HiperMaximus I think the team has decided to move forward with #1649 instead

@Kludex
Copy link
Sponsor Member

Kludex commented Aug 18, 2022

Let's go with #1649.

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

Successfully merging this pull request may close these issues.

None yet

8 participants