1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
|
Creating Middleware
===================
As mentioned in :ref:`using middleware <using-middleware>`, a middleware in Litestar
is **any callable** that takes a kwarg called ``app``, which is the next ASGI handler, i.e. an
:class:`~litestar.types.ASGIApp`, and returns an ``ASGIApp``.
The example previously given was using a factory function, i.e.:
.. code-block:: python
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
Extending ``ASGIMiddleware``
----------------------------
While using functions is a perfectly viable approach, the recommended way to handle this
is by using the :class:`~litestar.middleware.ASGIMiddleware` abstract base class, which
also includes functionality to dynamically skip the middleware based on ASGI
``scope["type"]``, handler ``opt`` keys or path patterns and a simple way to pass
configuration to middlewares; It does not implement an ``__init__`` method, so
subclasses are free to use it to customize the middleware's configuration.
Modifying Requests and Responses
++++++++++++++++++++++++++++++++
Middlewares can not only be used to execute *around* other ASGI callable, they can also
intercept and modify both incoming and outgoing data in a request / response cycle by
"wrapping" the respective ``receive`` and ``send`` ASGI callables.
The following demonstrates how to add a request timing header with a timestamp to all
outgoing responses:
.. literalinclude:: /examples/middleware/request_timing.py
:language: python
Migrating from ``MiddlewareProtocol`` / ``AbstractMiddleware``
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
:class:`~litestar.middleware.ASGIMiddleware` was introduced in Litestar 2.15. If you've
been using ``MiddlewareProtocol`` / ``AbstractMiddleware`` to implement your middlewares
before, there's a simple migration path to using ``ASGIMiddleware``.
**From MiddlewareProtocol**
.. tab-set::
.. tab-item:: MiddlewareProtocol
.. literalinclude:: /examples/middleware/middleware_protocol_migration_old.py
:language: python
.. tab-item:: ASGIMiddleware
.. literalinclude:: /examples/middleware/middleware_protocol_migration_new.py
:language: python
**From AbstractMiddleware**
.. tab-set::
.. tab-item:: MiddlewareProtocol
.. literalinclude:: /examples/middleware/abstract_middleware_migration_old.py
:language: python
.. tab-item:: ASGIMiddleware
.. literalinclude:: /examples/middleware/abstract_middleware_migration_new.py
:language: python
Using MiddlewareProtocol
------------------------
The :class:`~litestar.middleware.base.MiddlewareProtocol` class is a
`PEP 544 Protocol <https://peps.python.org/pep-0544/>`_ that specifies the minimal implementation of a middleware as
follows:
.. code-block:: python
from typing import Protocol, Any
from litestar.types import ASGIApp, Scope, Receive, Send
class MiddlewareProtocol(Protocol):
def __init__(self, app: ASGIApp, **kwargs: Any) -> None: ...
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ...
The ``__init__`` method receives and sets "app". *It's important to understand* that app is not an instance of Litestar in
this case, but rather the next middleware in the stack, which is also an ASGI app.
The ``__call__`` method makes this class into a ``callable``, i.e. once instantiated this class acts like a function, that
has the signature of an ASGI app: The three parameters, ``scope, receive, send`` are specified
by `the ASGI specification <https://asgi.readthedocs.io/en/latest/index.html>`_, and their values originate with the ASGI
server (e.g. ``uvicorn``\ ) used to run Litestar.
To use this protocol as a basis, simply subclass it - as you would any other class, and implement the two methods it
specifies:
.. code-block:: python
import logging
from litestar.types import ASGIApp, Receive, Scope, Send
from litestar import Request
from litestar.middleware.base import MiddlewareProtocol
logger = logging.getLogger(__name__)
class MyRequestLoggingMiddleware(MiddlewareProtocol):
def __init__(self, app: ASGIApp) -> None: # can have other parameters as well
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
request = Request(scope)
logger.info("Got request: %s - %s", request.method, request.url)
await self.app(scope, receive, send)
.. important::
Although ``scope`` is used to create an instance of request by passing it to the
:class:`~litestar.connection.Request` constructor, which makes it simpler to access because it does some parsing
for you already, the actual source of truth remains ``scope`` - not the request. If you need to modify the data of
the request you must modify the scope object, not any ephemeral request objects created as in the above.
Responding using the MiddlewareProtocol
+++++++++++++++++++++++++++++++++++++++
Once a middleware finishes doing whatever its doing, it should pass ``scope``, ``receive``, and ``send`` to an ASGI app
and await it. This is what's happening in the above example with: ``await self.app(scope, receive, send)``. Let's
explore another example - redirecting the request to a different url from a middleware:
.. code-block:: python
from litestar.types import ASGIApp, Receive, Scope, Send
from litestar.response.redirect import ASGIRedirectResponse
from litestar import Request
from litestar.middleware.base import MiddlewareProtocol
class RedirectMiddleware(MiddlewareProtocol):
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if Request(scope).session is None:
response = ASGIRedirectResponse(path="/login")
await response(scope, receive, send)
else:
await self.app(scope, receive, send)
As you can see in the above, given some condition (``request.session`` being ``None``) we create a
:class:`~litestar.response.redirect.ASGIRedirectResponse` and then await it. Otherwise, we await ``self.app``
Modifying ASGI Requests and Responses using the MiddlewareProtocol
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. important::
If you'd like to modify a :class:`~litestar.response.Response` object after it was created for a route
handler function but before the actual response message is transmitted, the correct place to do this is using the
special life-cycle hook called :ref:`after_request <after_request>`. The instructions in this section are for how to
modify the ASGI response message itself, which is a step further in the response process.
Using the :class:`~litestar.middleware.base.MiddlewareProtocol` you can intercept and modifying both the
incoming and outgoing data in a request / response cycle by "wrapping" that respective ``receive`` and ``send`` ASGI
functions.
To demonstrate this, let's say we want to append a header with a timestamp to all outgoing responses. We could achieve
this by doing the following:
.. code-block:: python
import time
from litestar.datastructures import MutableScopeHeaders
from litestar.types import Message, Receive, Scope, Send
from litestar.middleware.base import MiddlewareProtocol
from litestar.types import ASGIApp
class ProcessTimeHeader(MiddlewareProtocol):
def __init__(self, app: ASGIApp) -> None:
super().__init__(app)
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
start_time = time.monotonic()
async def send_wrapper(message: Message) -> None:
if message["type"] == "http.response.start":
process_time = time.monotonic() - start_time
headers = MutableScopeHeaders.from_message(message=message)
headers["X-Process-Time"] = str(process_time)
await send(message)
await self.app(scope, receive, send_wrapper)
else:
await self.app(scope, receive, send)
Inheriting AbstractMiddleware
-----------------------------
Litestar offers an :class:`~litestar.middleware.base.AbstractMiddleware` class that can be extended to
create middleware:
.. code-block:: python
import time
from litestar.enums import ScopeType
from litestar.middleware import AbstractMiddleware
from litestar.datastructures import MutableScopeHeaders
from litestar.types import Message, Receive, Scope, Send
class MyMiddleware(AbstractMiddleware):
scopes = {ScopeType.HTTP}
exclude = ["first_path", "second_path"]
exclude_opt_key = "exclude_from_middleware"
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
start_time = time.monotonic()
async def send_wrapper(message: "Message") -> None:
if message["type"] == "http.response.start":
process_time = time.monotonic() - start_time
headers = MutableScopeHeaders.from_message(message=message)
headers["X-Process-Time"] = str(process_time)
await send(message)
await self.app(scope, receive, send_wrapper)
The three class variables defined in the above example ``scopes``, ``exclude``, and ``exclude_opt_key`` can be used to
fine-tune for which routes and request types the middleware is called:
- The scopes variable is a set that can include either or both : ``ScopeType.HTTP`` and ``ScopeType.WEBSOCKET`` , with the default being both.
- ``exclude`` accepts either a single string or list of strings that are compiled into a regex against which the request's ``path`` is checked.
- ``exclude_opt_key`` is the key to use for in a route handler's :class:`Router.opt <litestar.router.Router>` dict for a boolean, whether to omit from the middleware.
Thus, in the following example, the middleware will only run against the handler called ``not_excluded_handler`` for ``/greet`` route:
.. literalinclude:: /examples/middleware/base.py
:language: python
.. danger::
Using ``/`` as an exclude pattern, will disable this middleware for all routes,
since, as a regex, it matches *every* path
Using DefineMiddleware to pass arguments
----------------------------------------
Litestar offers a simple way to pass positional arguments (``*args``) and keyword arguments (``**kwargs``) to middleware
using the :class:`~litestar.middleware.base.DefineMiddleware` class. Let's extend
the factory function used in the examples above to take some args and kwargs and then use ``DefineMiddleware`` to pass
these values to our middleware:
.. code-block:: python
from litestar.types import ASGIApp, Scope, Receive, Send
from litestar import Litestar
from litestar.middleware import DefineMiddleware
def middleware_factory(my_arg: int, *, app: ASGIApp, my_kwarg: str) -> ASGIApp:
async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None:
# here we can use my_arg and my_kwarg for some purpose
...
await app(scope, receive, send)
return my_middleware
app = Litestar(
route_handlers=[...],
middleware=[DefineMiddleware(middleware_factory, 1, my_kwarg="abc")],
)
The ``DefineMiddleware`` is a simple container - it takes a middleware callable as a first parameter, and then any
positional arguments, followed by key word arguments. The middleware callable will be called with these values as well
as the kwarg ``app`` as mentioned above.
|