Skip to content

Commit 3d1b011

Browse files
committed
feat(middleware): Add should_bypass_for_scope to ASGIMiddleware to allow excluding middlewares dynamically (#4441)
(cherry picked from commit 94073d3)
1 parent c7b9b1c commit 3d1b011

File tree

2 files changed

+46
-0
lines changed

2 files changed

+46
-0
lines changed

litestar/middleware/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,29 @@ async def handle(
217217
exclude_path_pattern: str | tuple[str, ...] | None = None
218218
exclude_opt_key: str | None = None
219219

220+
should_bypass_for_scope: Callable[[Scope], bool] | None = None
221+
r"""
222+
A callable that takes in the :class:`~litestar.types.Scope` of the current
223+
connection and returns a boolean, indicating if the middleware should be skipped for
224+
the current request.
225+
226+
This can for example be used to exclude a middleware based on a dynamic path::
227+
228+
should_bypass_for_scope = lambda scope: scope["path"].endswith(".jpg")
229+
230+
Applied to a route with a dynamic path like ``/static/{file_name:str}``, it would
231+
be skipped *only* if ``file_name`` has a ``.jpg`` extension.
232+
233+
.. note::
234+
235+
If it is not required to dynamically match the path of a request,
236+
:attr:`~litestar.middleware.ASGIMiddleware.exclude_path_pattern` should be
237+
used instead. Since its exclusion is done statically at startup time, it has no
238+
performance cost at runtime.
239+
240+
.. versionadded:: 2.19
241+
"""
242+
220243
def __call__(self, app: ASGIApp) -> ASGIApp:
221244
"""Create the actual middleware callable"""
222245
handle = self.handle

tests/unit/test_middleware/test_base_middleware.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,29 @@ def third_handler() -> dict:
285285
assert "test" in response.headers
286286

287287

288+
def test_asgi_middleware_should_exclude_scope() -> None:
289+
mock = MagicMock()
290+
291+
class SubclassMiddleware(ASGIMiddleware):
292+
@staticmethod
293+
def should_bypass_for_scope(scope: "Scope") -> bool:
294+
return scope["path"].endswith(".jpg")
295+
296+
async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None:
297+
mock(scope["path"])
298+
await next_app(scope, receive, send)
299+
300+
@get("/{file_name:str}")
301+
def handler(file_name: str) -> str:
302+
return file_name
303+
304+
with create_test_client([handler], middleware=[SubclassMiddleware()]) as client:
305+
assert client.get("/test.txt").status_code == 200
306+
assert client.get("/test.jpg").status_code == 200
307+
308+
mock.assert_called_once_with("/test.txt")
309+
310+
288311
@pytest.mark.parametrize("excludes", ["/", ("/", "/foo"), "/*", "/.*"])
289312
def test_asgi_middleware_exclude_by_pattern_warns_if_exclude_all(excludes: Union[str, Tuple[str, ...]]) -> None:
290313
class SubclassMiddleware(ASGIMiddleware):

0 commit comments

Comments
 (0)