[Document] Order of middlewares
Closed this issue · 7 comments
I just started a project with starlette awesome, and found a small issue about the docs related on the order of execution of middlewares... My case, is so usual, like in django:
auth_middleware that depends on session_middleware
Seems like the order is inverse on the order you declare them:
app.add_middleware(AuthenticationMiddleware, backend=middlewares.AuthBackend())
app.add_middleware(middlewares.SessionMiddleware)
The session one is executed first but needs to be added later. I'm not against it, it's an onion, and can be seen in both directions... But I thought that at least should be documented.
On the API side, perhaps is a nice idea, to just instantiate them on a single call, like:
app.add_middlewares([
middlewareX, middlewareY # in order of execution... (at least when the request is entering..)
])
Yeah, I'm not a fan of how it is at the moment, but it's a bit unavoidable with the current style. (We can only wrap middleware around what's already been added.)
And yes, we'll eventually do something more like this...
middleware = [
Middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS),
Middleware(HTTPSRedirectMiddleware, enabled=not DEBUG),
Middleware(SessionMiddleware, backend=CookieSignedSessions(secret_key=SECRET_KEY)),
Middleware(AuthMiddleware, backend=DatabaseAuthBackend(model=User)),
]
app = Starlette(
debug=DEBUG,
routes=routes,
events=events,
middleware=middleware,
exception_handlers=exception_handlers
)
See #396
Something like this?
class App(Starlette):
async def __call__(self, scope, receive, send):
scope['app'] = self
# middlewares could be a list
# and also some kind of predicates could be added
# to just at runtime, change the middlware chain.
# or the result, could just be cached.
result = compose(
self.router,
(TransactionMiddleware, ),
(ExceptionMiddleware, dict(debug=self.debug)),
(ServerErrorMiddleware, dict(debug=self.debug))
)
await result(scope, receive, send)
def compose(initial, *args):
base = initial
for arg in args:
if len(arg) == 2:
klass, kwargs = arg
base = klass(base, **kwargs)
elif len(arg) == 1:
klass = arg[0]
base = klass(base)
return base
app = App(debug=True)
had the same issue, just adding some documentation would be great as this is a pretty opaque bug to fix.
I think this was achieved in Version 0.13.0. From the current docs:
Every Starlette application automatically includes two pieces of middleware
by default:
ServerErrorMiddleware
- Ensures that application exceptions may return a custom 500 page, or display an application traceback in DEBUG mode. This is always the outermost middleware layer.ExceptionMiddleware
- Adds exception handlers, so that particular types of expected exception cases can be associated with handler functions. For example raisingHTTPException(status_code=404)
within an endpoint will end up rendering a custom 404 page.Middleware is evaluated from top-to-bottom, so the flow of execution in our example
application would look like this:
- Middleware
ServerErrorMiddleware
TrustedHostMiddleware
HTTPSRedirectMiddleware
ExceptionMiddleware
- Routing
- Endpoint
So this can be closed now.
For anyone landing here trying to understand Middleware execution order, I did some testing with 3 custom middleware.
class Middleware:
def __init__(self, app, name) -> None:
self.app = app
self.name = str(name)
async def __call__(self, scope, receive, send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def new_receive():
print("Receive: " + self.name)
return await receive()
async def new_send(message) -> None:
print("Send: " + self.name)
return await send(message)
try:
print("Before: " + self.name)
await self.app(scope, new_receive, new_send)
print("After: " + self.name)
except:
print("Exception: " + self.name)
raise
They were added in this order
app.add_middleware(Middleware, name=1)
app.add_middleware(Middleware, name=2)
app.add_middleware(Middleware, name=3)
And here were the execution results
Before: 3
Before: 2
Before: 1
Receive: 1
Receive: 2
Receive: 3
Send: 1
Send: 2
Send: 3
After: 1
After: 3
After: 3
You can see they execute in the expected order for everything except for code ran prior to awaiting the app.
If an Exception is raised in the route, you lose all theAfter
code and instead get
Exception: 1
Exception: 2
Exception: 3
Also keep in mind, if a middleware suppresses an error by not re-raising it, you get something like
Exception: 1
After: 2
After: 3
Hope this helps someone.
@zpyoung Specifically in my case I've manage to resolve the error AssertionError: SessionMiddleware must be installed to access request.session
following your understanding: A LIFO ordering, first the custom middleware, then their dependancies/"native" middleware.
# Custom:
app.add_middleware(TenantMiddleware)
# Custom:
app.add_middleware(AppUserAuthMiddleware)
# "Native" & basis for `AppUserAuthMiddleware`
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
# "Native"
app.add_middleware(CORSMiddleware, **CORS_SETTINGS)
Thank you!