diff --git a/starlette/routing.py b/starlette/routing.py index 1aa2cdb6d..7253321cc 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -234,7 +234,11 @@ def __init__( def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]: if scope["type"] == "http": - match = self.path_regex.match(scope["path"]) + root_path = scope.get("current_root_path", scope.get("root_path", "")) + path = scope.get( + "current_path", re.sub(r"^" + root_path, "", scope["path"]) + ) + match = self.path_regex.match(path) if match: matched_params = match.groupdict() for key, value in matched_params.items(): @@ -370,21 +374,25 @@ def routes(self) -> typing.List[BaseRoute]: def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]: if scope["type"] in ("http", "websocket"): path = scope["path"] - match = self.path_regex.match(path) + root_path = scope.get("current_root_path", scope.get("root_path", "")) + current_path = scope.get("current_path", re.sub(r"^" + root_path, "", path)) + match = self.path_regex.match(current_path) if match: matched_params = match.groupdict() for key, value in matched_params.items(): matched_params[key] = self.param_convertors[key].convert(value) remaining_path = "/" + matched_params.pop("path") - matched_path = path[: -len(remaining_path)] + matched_path = current_path[: -len(remaining_path)] path_params = dict(scope.get("path_params", {})) path_params.update(matched_params) root_path = scope.get("root_path", "") child_scope = { "path_params": path_params, "app_root_path": scope.get("app_root_path", root_path), - "root_path": root_path + matched_path, - "path": remaining_path, + "current_root_path": root_path + matched_path, + "current_path": remaining_path, + "root_path": root_path, + "path": path, "endpoint": self.app, } return Match.FULL, child_scope diff --git a/tests/test_routing.py b/tests/test_routing.py index e3b1e412a..3b7cf54da 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -549,6 +549,60 @@ def test_url_for_with_root_path(test_client_factory): } +async def echo_paths(request, name): + return JSONResponse( + { + "name": name, + "path": request.scope["path"], + "root_path": request.scope["root_path"], + } + ) + + +echo_paths_routes = [ + Route( + "/path", + functools.partial(echo_paths, name="path"), + name="path", + methods=["GET"], + ), + Mount( + "/root", + name="mount", + routes=[ + Route( + "/path", + functools.partial(echo_paths, name="subpath"), + name="subpath", + methods=["GET"], + ) + ], + ), +] + + +def test_paths_with_root_path(test_client_factory): + app = Starlette(routes=echo_paths_routes) + client = test_client_factory( + app, base_url="https://www.example.org/", root_path="/root" + ) + response = client.get("/root/path") + assert response.status_code == 200 + assert response.json() == { + "name": "path", + "path": "/root/path", + "root_path": "/root", + } + + response = client.get("/root/root/path") + assert response.status_code == 200 + assert response.json() == { + "name": "subpath", + "path": "/root/root/path", + "root_path": "/root", + } + + async def stub_app(scope, receive, send): pass # pragma: no cover