Using Middleware#

A middleware in Litestar is any callable that receives at least one kwarg called app and returns an ASGIApp. An ASGIApp is nothing but an async function that receives the ASGI primitives scope , receive and send , and either calls the next ASGIApp or returns a response / handles the websocket connection.

For example, the following function can be used as a middleware because it receives the app kwarg and returns an ASGIApp:

from litestar.types import ASGIApp, Scope, Receive, Send


def middleware_factory(app: ASGIApp) -> ASGIApp:
    async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None:
        # do something here
        ...
        await app(scope, receive, send)

    return my_middleware

We can then pass this middleware to the Litestar instance, where it will be called on every request:

from litestar.types import ASGIApp, Scope, Receive, Send
from litestar import Litestar


def middleware_factory(app: ASGIApp) -> ASGIApp:
    async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None:
        # do something here
        ...
        await app(scope, receive, send)

    return my_middleware


app = Litestar(route_handlers=[...], middleware=[middleware_factory])

In the above example, Litestar will call the middleware_factory function and pass to it app. It’s important to understand that this kwarg does not designate the Litestar application but rather the next ASGIApp in the stack. It will then insert the returned my_middleware function into the stack of every route in the application - because we declared it on the application level.

Layered architecture

Middlewares are part of Litestar’s layered architecture* which means you can set them on every layer of the application.

You can read more about this here: Layered architecture

Middleware Call Order#

Since it’s also possible to define multiple middlewares on every layer, the call order for middlewares will be top to bottom and left to right. This means for each layer, the middlewares will be called in the order they have been passed, while the layers will be traversed in the usual order:

        flowchart LR
    Application --> Router --> Controller --> Handler
    
from typing import TYPE_CHECKING, List, Type

from litestar import Controller, Litestar, Router, get
from litestar.datastructures import State
from litestar.middleware import MiddlewareProtocol

if TYPE_CHECKING:
    from litestar.types import ASGIApp, Receive, Scope, Send


def create_test_middleware(middleware_id: int) -> Type[MiddlewareProtocol]:
    class TestMiddleware(MiddlewareProtocol):
        def __init__(self, app: "ASGIApp") -> None:
            self.app = app

        async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
            litestar_app = scope["app"]
            litestar_app.state.setdefault("middleware_calls", [])
            litestar_app.state["middleware_calls"].append(middleware_id)
            await self.app(scope, receive, send)

    return TestMiddleware


class MyController(Controller):
    path = "/controller"
    middleware = [create_test_middleware(4), create_test_middleware(5)]

    @get(
        "/handler",
        middleware=[create_test_middleware(6), create_test_middleware(7)],
    )
    async def my_handler(self, state: State) -> List[int]:
        return state["middleware_calls"]


router = Router(
    path="/router",
    route_handlers=[MyController],
    middleware=[create_test_middleware(2), create_test_middleware(3)],
)

app = Litestar(
    route_handlers=[router],
    middleware=[create_test_middleware(0), create_test_middleware(1)],
)
from typing import TYPE_CHECKING

from litestar import Controller, Litestar, Router, get
from litestar.datastructures import State
from litestar.middleware import MiddlewareProtocol

if TYPE_CHECKING:
    from litestar.types import ASGIApp, Receive, Scope, Send


def create_test_middleware(middleware_id: int) -> type[MiddlewareProtocol]:
    class TestMiddleware(MiddlewareProtocol):
        def __init__(self, app: "ASGIApp") -> None:
            self.app = app

        async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
            litestar_app = scope["app"]
            litestar_app.state.setdefault("middleware_calls", [])
            litestar_app.state["middleware_calls"].append(middleware_id)
            await self.app(scope, receive, send)

    return TestMiddleware


class MyController(Controller):
    path = "/controller"
    middleware = [create_test_middleware(4), create_test_middleware(5)]

    @get(
        "/handler",
        middleware=[create_test_middleware(6), create_test_middleware(7)],
    )
    async def my_handler(self, state: State) -> list[int]:
        return state["middleware_calls"]


router = Router(
    path="/router",
    route_handlers=[MyController],
    middleware=[create_test_middleware(2), create_test_middleware(3)],
)

app = Litestar(
    route_handlers=[router],
    middleware=[create_test_middleware(0), create_test_middleware(1)],
)

Run it

> curl http://127.0.0.1:8000/router/controller/handler
[0,1,2,3,4,5,6,7]

Middlewares and Exceptions#

When an exception is raised by a route handler or a dependency it will be transformed into a response by an exception handler. This response will follow the normal “flow” of the application and therefore, middlewares are still applied to it.

As with any good rule, there are exceptions to it. In this case they are two exceptions raised by Litestar’s ASGI router:

They are raised before the middleware stack is called and will only be handled by exception handlers defined on the Litestar instance itself. If you wish to modify error responses generated from these exception, you will have to use an application layer exception handler.