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

Add Mount(..., middleware=[...]) #1649

Merged
merged 34 commits into from
Sep 21, 2022
Merged

Conversation

adriangb
Copy link
Member

@adriangb adriangb commented May 24, 2022

docs/middleware.md Outdated Show resolved Hide resolved
docs/middleware.md Outdated Show resolved Hide resolved
tests/test_routing.py Outdated Show resolved Hide resolved
@Kludex Kludex added this to the Version 0.21.0 milestone May 27, 2022
@Kludex Kludex added the feature New feature or request label May 27, 2022
@tkrause
Copy link

tkrause commented Jul 12, 2022

This would be really useful. Is there a plan to merge and release this still at some point?

@Kludex
Copy link
Member

Kludex commented Jul 12, 2022

I'll review this before the next minor release.

@progamesigner
Copy link

The implementation is simple and works well. However, there is a different behavior in both if an exception raised in handler.

This exception will be handled by ExceptionMiddleware and thus app-level middleware will still be able to change response such as appending extra tracking header. But this is not the case using router-level middleware (wherher #1649 or #1464).

I wonder if this behavior changing is expected and should be documented or we need build the middleware stack per Mount or Router? I currently remove the app-level middleware and wrap router-level middleware between ServerErrorMiddleware and ExceptionMiddleware.

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
@Kludex
Copy link
Member

Kludex commented Sep 1, 2022

I think we can take this opportunity to add a more real case usage instead of GZipMiddleware, maybe a middleware related to the issue that motivates this PR. What do you think?

@adriangb
Copy link
Member Author

adriangb commented Sep 1, 2022

I think we can take this opportunity to add a more real case usage instead of GZipMiddleware, maybe a middleware related to the issue that motivates this PR. What do you think?

I was going to say yes but: #1837. So GZipMiddleware is actually a motivating example, right?

@Kludex
Copy link
Member

Kludex commented Sep 1, 2022

I think we can take this opportunity to add a more real case usage instead of GZipMiddleware, maybe a middleware related to the issue that motivates this PR. What do you think?

I was going to say yes but: #1837. So GZipMiddleware is actually a motivating example, right?

Copy link
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @adriangb 🎉

I think it will be great if we can add a test that fails with the behavior you mentioned on the documentation, and I know you like those kinds of tests... 😎 👍 What do you think?

A bit of reference about this PR, because my memory is short, and I like to make things clear:

There were two previous attempts to solve the issue, which the first one was actually approved (#1286), but I pushed back. On the second one (#1464), @florimondmanca suggested adding a middleware parameter only on the Mount class, and other members agreed, so it was rejected as well. And now, I think we found the solution that makes more sense. Short solution, great improvement. 👍

docs/middleware.md Outdated Show resolved Hide resolved
tests/test_routing.py Outdated Show resolved Hide resolved
"route",
[
mounted_routes_with_middleware,
mounted_routes_with_middleware,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated as well, so I think we can get rid of the @parametrize.

Copy link
Member Author

@adriangb adriangb Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eee6a6f

route: BaseRoute,
) -> None:
test_client = test_client_factory(Router([route]))
response = test_client.get("/http")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also assert that calling a route not under the Mount does not apply the middleware? E.g. testing a test_client.get("/").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in eee6a6f

Comment on lines 711 to 717
Note that middleware used in this way is *not* wrapped in exception handling middleware like the middleware applied to the `Starlette` application is.
This is often not a problem because it only applies to middleware that inspect or modify the `Response`, and even then you probably don't want to apply this logic to error responses.
If you do want to apply the middelware logic to error responses only on some routes you have a couple of options:

* Add an `ExceptionMiddleware` onto the `Mount`
* Add a `try/except` block to your middleware and return an error response from there
* Split up marking and processing into two middlewares, one that gets put on `Mount` which simply marks the response as needing processing (for example by setting `scope["log-response"] = True`) and another applied to the `Starlette` application that does the heavy lifting.
Copy link
Member

@florimondmanca florimondmanca Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why requests to sub-mounted error routes bypass middleware associated with that mount? Or maybe that's not the problem here? I'm confused. Isn't ErrorMiddleware supposed to wrap the entire Starlette.router, which includes sub-apps?

docs/middleware.md Outdated Show resolved Hide resolved
Comment on lines 711 to 717
Note that middleware used in this way is *not* wrapped in exception handling middleware like the middleware applied to the `Starlette` application is.
This is often not a problem because it only applies to middleware that inspect or modify the `Response`, and even then you probably don't want to apply this logic to error responses.
If you do want to apply the middelware logic to error responses only on some routes you have a couple of options:

* Add an `ExceptionMiddleware` onto the `Mount`
* Add a `try/except` block to your middleware and return an error response from there
* Split up marking and processing into two middlewares, one that gets put on `Mount` which simply marks the response as needing processing (for example by setting `scope["log-response"] = True`) and another applied to the `Starlette` application that does the heavy lifting.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lemme try to draw this out to make sure I understand...

Looking at the Starlette code, we have this layering:

routes = [
    Route("/", ...),
    Mount("/sub", sub, middleware=[Middleware(MountMiddlewareA)]),
]

app = Starlette(
    routes=routes,
    middleware=[Middleware(MiddlewareB), Middleware(MiddlewareC)],
)
ServerErrorMiddleware(
  MiddlewareC(
    MiddlewareB(
      ExceptionMiddleware(
        Router(
          routes=[
            Route("/", ...),
            Mount("/sub", MiddlewareA(sub))
          ]
        )
      )
    )
  )
)

So, if an exception occurs in /, it gets handled by ExceptionMiddleware and Middleware{B,C} get a well-defined response [perhaps an error response] to process.

But if an exception occurs in /sub*, it gets sent to MiddlewareA, which has to handle it if it hopes to do some response processing (otherwise that processing isn't run).

Is that right?

@Kludex Kludex added the hold Don't merge it label Sep 3, 2022
@adriangb
Copy link
Member Author

I'm going to go ahead and merge it so we can proceed with the next release, if anyone has any last minute concerns we can address them in a separate PR before the release.

@adriangb
Copy link
Member Author

A thought about this PR, Request objects and error handling. I wonder if we maybe should’ve had ExceptionMiddleware wrap each Route instead of being a middleware at the application level? That would have both resolved the concerns in #1649 (comment) and also #1692 (comment) because we’d be able to (1) make sure there is nothing in between ExceptionMiddleware and the route and (2) use the same Request object for error handlers and the endpoint. I wonder if this could be made backwards compatible…

aminalaee pushed a commit that referenced this pull request Feb 13, 2023
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
Co-authored-by: Aber <me@abersheeran.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request hold Don't merge it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Include routing information for middleware
7 participants