diff --git a/starlette/requests.py b/starlette/requests.py index a33367e1d..f4b8692d1 100644 --- a/starlette/requests.py +++ b/starlette/requests.py @@ -95,14 +95,23 @@ def url(self) -> URL: return self._url @property - def base_url(self) -> URL: - if not hasattr(self, "_base_url"): + def app_root_base_url(self) -> URL: + if not hasattr(self, "_app_root_base_url"): base_url_scope = dict(self.scope) base_url_scope["path"] = "/" base_url_scope["query_string"] = b"" base_url_scope["root_path"] = base_url_scope.get( "app_root_path", base_url_scope.get("root_path", "") ) + self._app_root_base_url = URL(scope=base_url_scope) + return self._app_root_base_url + + @property + def base_url(self) -> URL: + if not hasattr(self, "_base_url"): + base_url_scope = dict(self.scope) + base_url_scope["path"] = "/" + base_url_scope["query_string"] = b"" self._base_url = URL(scope=base_url_scope) return self._base_url @@ -170,9 +179,16 @@ def state(self) -> State: return self._state def url_for(self, name: str, **path_params: typing.Any) -> str: - router: Router = self.scope["router"] - url_path = router.url_path_for(name, **path_params) - return url_path.make_absolute_url(base_url=self.base_url) + app_root_router = self.scope["router"] + app_router = getattr(self.get('app', {}), 'router', None) + if app_router and app_router != app_root_router: + try: + url_path = app_router.url_path_for(name, **path_params) + return url_path.make_absolute_url(base_url=self.base_url) + except Exception: + pass + url_path = app_root_router.url_path_for(name, **path_params) + return url_path.make_absolute_url(base_url=self.app_root_base_url) async def empty_receive() -> typing.NoReturn: diff --git a/tests/test_routing.py b/tests/test_routing.py index 231c581fb..d6fc5473b 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -517,6 +517,45 @@ def test_url_for_with_root_path(test_client_factory): } + +def echo_urls_for(*keys): + def echo_urls(request): + def try_url_for(name): + try: + return request.url_for(name) + except NoMatchFound: + return 'NoMatchFound' + return JSONResponse({ k: try_url_for(k) for k in keys }) + return echo_urls + +def test_url_with_sub_app(test_client_factory): + sub_app = Starlette( + routes=[ + Route("/", echo_urls_for('subapp_index','submount:subapp_index','index'), name="subapp_index", methods=["GET"]) + ] + ) + app = Starlette( + routes=[ + Route("/", echo_urls_for('index', 'submount:subapp_index'), name="index", methods=["GET"]), + Mount('/submount', app=sub_app, name='submount') + ] + ) + + client = test_client_factory( + app, base_url="https://www.example.org/", root_path="/sub_path" + ) + response = client.get("/") + assert response.json() == { + "index": "https://www.example.org/sub_path/", + "submount:subapp_index": "https://www.example.org/sub_path/submount/", + } + response = client.get("/submount/") + assert response.json() == { + "submount:subapp_index": "https://www.example.org/sub_path/submount/", + "subapp_index": "https://www.example.org/sub_path/submount/", + "index": "https://www.example.org/sub_path/", + } + async def stub_app(scope, receive, send): pass # pragma: no cover