File: creating-middleware.rst

package info (click to toggle)
litestar 2.21.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 12,568 kB
  • sloc: python: 70,588; makefile: 254; javascript: 104; sh: 60
file content (311 lines) | stat: -rw-r--r-- 12,164 bytes parent folder | download
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.