Skip to content

Commit 57945ef

Browse files
committed
backport should_bypass_for_scope
1 parent 3d1b011 commit 57945ef

File tree

3 files changed

+82
-11
lines changed

3 files changed

+82
-11
lines changed

litestar/middleware/_utils.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ def build_exclude_path_pattern(
4444
) from e
4545

4646

47+
def should_bypass_for_path_pattern(scope: Scope, pattern: Pattern | None = None) -> bool:
48+
return bool(
49+
pattern
50+
and pattern.findall(
51+
scope["raw_path"].decode() if getattr(scope.get("route_handler", {}), "is_mount", False) else scope["path"]
52+
)
53+
)
54+
55+
4756
def should_bypass_middleware(
4857
*,
4958
exclude_http_methods: Sequence[Method] | None = None,
@@ -73,9 +82,4 @@ def should_bypass_middleware(
7382
if exclude_http_methods and scope.get("method") in exclude_http_methods:
7483
return True
7584

76-
return bool(
77-
exclude_path_pattern
78-
and exclude_path_pattern.findall(
79-
scope["raw_path"].decode() if getattr(scope.get("route_handler", {}), "is_mount", False) else scope["path"]
80-
)
81-
)
85+
return should_bypass_for_path_pattern(scope, exclude_path_pattern)

litestar/middleware/base.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

33
import abc
4+
import warnings
45
from abc import abstractmethod
56
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
67

78
from litestar.enums import ScopeType
89
from litestar.middleware._utils import (
910
build_exclude_path_pattern,
11+
should_bypass_for_path_pattern,
1012
should_bypass_middleware,
1113
)
1214
from litestar.utils.deprecation import warn_deprecation
@@ -246,16 +248,48 @@ def __call__(self, app: ASGIApp) -> ASGIApp:
246248
exclude_pattern = build_exclude_path_pattern(exclude=self.exclude_path_pattern, middleware_cls=type(self))
247249
scopes = set(self.scopes)
248250
exclude_opt_key = self.exclude_opt_key
251+
should_bypass_for_scope = self.should_bypass_for_scope
252+
253+
def exclude_pattern_matches_handler_path(scope: Scope) -> bool:
254+
if exclude_pattern is None:
255+
return False
256+
handler = scope["route_handler"]
257+
return any(exclude_pattern.search(path) for path in handler.paths)
249258

250259
async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
251-
if should_bypass_middleware(
252-
scope=scope,
253-
scopes=scopes, # type: ignore[arg-type]
254-
exclude_opt_key=exclude_opt_key,
255-
exclude_path_pattern=exclude_pattern,
260+
path_excluded = False
261+
if (
262+
should_bypass_middleware(
263+
scope=scope,
264+
scopes=scopes, # type: ignore[arg-type]
265+
exclude_opt_key=exclude_opt_key,
266+
)
267+
or (path_excluded := should_bypass_for_path_pattern(scope, exclude_pattern))
268+
or (should_bypass_for_scope and should_bypass_for_scope(scope))
256269
):
270+
if path_excluded and not exclude_pattern_matches_handler_path(scope):
271+
warnings.warn(
272+
f"{type(self).__name__}.exclude_path_pattern={exclude_pattern.pattern!r} "
273+
"did match the request path but did not match the route "
274+
"handler's path. When upgrading to Litestar 3, this middleware "
275+
"would NOT be excluded. To keep the current behaviour, use "
276+
"'should_bypass_for_scope' instead.",
277+
category=DeprecationWarning,
278+
stacklevel=2,
279+
)
280+
257281
await app(scope, receive, send)
258282
else:
283+
if exclude_pattern_matches_handler_path(scope):
284+
warnings.warn(
285+
f"{type(self).__name__}.exclude_path_pattern={exclude_pattern.pattern!r} "
286+
"did not match the request path but did match the route "
287+
"handler's path. When upgrading to Litestar 3, this middleware "
288+
"would be excluded. To keep the current behaviour, use "
289+
"'should_bypass_for_scope' instead.",
290+
category=DeprecationWarning,
291+
stacklevel=2,
292+
)
259293
await handle(scope=scope, receive=receive, send=send, next_app=app)
260294

261295
return middleware

tests/unit/test_middleware/test_base_middleware.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import TYPE_CHECKING, List, Tuple, Union
2+
from unittest.mock import MagicMock
23
from warnings import catch_warnings
34

45
import pytest
@@ -308,6 +309,38 @@ def handler(file_name: str) -> str:
308309
mock.assert_called_once_with("/test.txt")
309310

310311

312+
def test_asgi_middleware_path_exclude_warns_future_use() -> None:
313+
mock = MagicMock()
314+
315+
class SubclassMiddleware(ASGIMiddleware):
316+
def __init__(self, pattern: str) -> None:
317+
self.exclude_path_pattern = pattern
318+
319+
async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None:
320+
mock(scope["path"])
321+
await next_app(scope, receive, send)
322+
323+
@get("/{file_name:str}")
324+
def handler(file_name: str) -> str:
325+
return file_name
326+
327+
# this configuration would NOT be excluded in the future
328+
with create_test_client([handler], middleware=[SubclassMiddleware(".jpg")]) as client:
329+
with pytest.warns(DeprecationWarning, match=".*exclude_path_pattern.* did match the request path"):
330+
assert client.get("/test.jpg").status_code == 200
331+
332+
assert client.get("/test.txt").status_code == 200
333+
mock.assert_called_once_with("/test.txt")
334+
335+
mock.reset_mock()
336+
337+
# this configuration WOULD be excluded in the future
338+
with create_test_client([handler], middleware=[SubclassMiddleware("str")]) as client:
339+
with pytest.warns(DeprecationWarning, match=".*exclude_path_pattern.* did not match the request path"):
340+
assert client.get("/test.jpg").status_code == 200
341+
mock.assert_called_once_with("/test.jpg")
342+
343+
311344
@pytest.mark.parametrize("excludes", ["/", ("/", "/foo"), "/*", "/.*"])
312345
def test_asgi_middleware_exclude_by_pattern_warns_if_exclude_all(excludes: Union[str, Tuple[str, ...]]) -> None:
313346
class SubclassMiddleware(ASGIMiddleware):

0 commit comments

Comments
 (0)