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

docs: refactor "Custom Middleware" guide #3833

Merged
merged 2 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 13 additions & 21 deletions docs/examples/middleware/base.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
from time import time
from typing import TYPE_CHECKING, Dict
import time
from typing import Dict

from litestar import Litestar, get, websocket
from litestar import Litestar, WebSocket, get, websocket
from litestar.datastructures import MutableScopeHeaders
from litestar.enums import ScopeType
from litestar.middleware import AbstractMiddleware

if TYPE_CHECKING:
from litestar import WebSocket
from litestar.types import Message, Receive, Scope, Send
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"
exclude_opt_key = "exclude_from_my_middleware"

async def __call__(
self,
scope: "Scope",
receive: "Receive",
send: "Send",
) -> None:
start_time = time()
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() - start_time
process_time = time.monotonic() - start_time
headers = MutableScopeHeaders.from_message(message=message)
headers["X-Process-Time"] = str(process_time)
await send(message)
Expand All @@ -35,12 +27,12 @@ async def send_wrapper(message: "Message") -> None:


@websocket("/my-websocket")
async def websocket_handler(socket: "WebSocket") -> None:
async def websocket_handler(socket: WebSocket) -> None:
"""
Websocket handler - is excluded because the middleware scopes includes 'ScopeType.HTTP'
"""
await socket.accept()
await socket.send_json({"hello websocket"})
await socket.send_json({"hello": "websocket"})
await socket.close()


Expand All @@ -56,10 +48,10 @@ def second_handler() -> Dict[str, str]:
return {"hello": "second"}


@get("/third_path", exclude_from_middleware=True, sync_to_thread=False)
@get("/third_path", exclude_from_my_middleware=True, sync_to_thread=False)
def third_handler() -> Dict[str, str]:
"""Handler is excluded due to the opt key 'exclude_from_middleware' matching the middleware 'exclude_opt_key'."""
return {"hello": "second"}
"""Handler is excluded due to the opt key 'exclude_from_my_middleware' matching the middleware 'exclude_opt_key'."""
return {"hello": "third"}


@get("/greet", sync_to_thread=False)
Expand Down
60 changes: 27 additions & 33 deletions docs/usage/middleware/creating-middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
Creating Middleware
===================

As mentioned in :doc:`using middleware </usage/middleware/using-middleware>`, a middleware in Litestar
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:`ASGIApp <litestar.types.ASGIApp>`, and returns an ``ASGIApp``.
:class:`~litestar.types.ASGIApp`, and returns an ``ASGIApp``.

The example previously given was using a factory function, i.e.:

Expand All @@ -22,14 +22,14 @@ The example previously given was using a factory function, i.e.:
return my_middleware

While using functions is a perfectly viable approach, you can also use classes to do the same. See the next sections on
two base classes you can use for this purpose - the :class:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` ,
which gives a bare-bones type, or the :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` that offers a
two base classes you can use for this purpose - the :class:`~litestar.middleware.base.MiddlewareProtocol` ,
which gives a bare-bones type, or the :class:`~litestar.middleware.base.AbstractMiddleware` that offers a
base class with some built in functionality.

Using MiddlewareProtocol
------------------------

The :class:`MiddlewareProtocol <litestar.middleware.base.MiddlewareProtocol>` class is a
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:

Expand All @@ -50,7 +50,7 @@ this case, but rather the next middleware in the stack, which is also an ASGI ap
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.
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:
Expand All @@ -67,20 +67,19 @@ specifies:


class MyRequestLoggingMiddleware(MiddlewareProtocol):
def __init__(self, app: ASGIApp) -> None:
super().__init__(app)
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("%s - %s" % request.method, request.url)
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:`Request <.connection.Request>` constructor, which makes it simpler to access because it does some parsing
: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.

Expand All @@ -103,7 +102,6 @@ explore another example - redirecting the request to a different url from a midd

class RedirectMiddleware(MiddlewareProtocol):
def __init__(self, app: ASGIApp) -> None:
super().__init__(app)
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
Expand All @@ -113,24 +111,24 @@ explore another example - redirecting the request to a different url from a midd
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:`ASGIRedirectResponse <litestar.response.redirect.ASGIRedirectResponse>` and then await it. Otherwise, we await ``self.app``
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:`Response <.response.Response>` object after it was created for a route
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:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` you can intercept and modifying both the
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, lets say we want to append a header with a timestamp to all outgoing responses. We could achieve
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
Expand All @@ -150,11 +148,11 @@ this by doing the following:

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
start_time = time.time()
start_time = time.monotonic()

async def send_wrapper(message: Message) -> None:
if message["type"] == "http.response.start":
process_time = time.time() - start_time
process_time = time.monotonic() - start_time
headers = MutableScopeHeaders.from_message(message=message)
headers["X-Process-Time"] = str(process_time)
await send(message)
Expand All @@ -166,34 +164,30 @@ this by doing the following:
Inheriting AbstractMiddleware
-----------------------------

Litestar offers an :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` class that can be extended to
Litestar offers an :class:`~litestar.middleware.base.AbstractMiddleware` class that can be extended to
create middleware:

.. code-block:: python

from typing import TYPE_CHECKING
from time import time
import time

from litestar.enums import ScopeType
from litestar.middleware import AbstractMiddleware
from litestar.datastructures import MutableScopeHeaders


if TYPE_CHECKING:
from litestar.types import Message, Receive, Scope, Send
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()
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() - start_time
process_time = time.monotonic() - start_time
headers = MutableScopeHeaders.from_message(message=message)
headers["X-Process-Time"] = str(process_time)
await send(message)
Expand All @@ -204,11 +198,11 @@ The three class variables defined in the above example ``scopes``, ``exclude``,
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.
- 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 ``opt`` dict for a boolean, whether to omit from the middleware.
- ``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 route handler called ``not_excluded_handler``:
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
Expand All @@ -222,8 +216,8 @@ Thus, in the following example, the middleware will only run against the route h
Using DefineMiddleware to pass arguments
----------------------------------------

Litestar offers a simple way to pass positional arguments (``*args``) and key-word arguments (``**kwargs``) to middleware
using the :class:`DefineMiddleware <litestar.middleware.base.DefineMiddleware>` class. Let's extend
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:

Expand Down
2 changes: 2 additions & 0 deletions docs/usage/middleware/using-middleware.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _using-middleware:

Using Middleware
================

Expand Down
Loading