diff --git a/CHANGES/11673.bugfix.rst b/CHANGES/11673.bugfix.rst new file mode 100644 index 00000000000..accbe850847 --- /dev/null +++ b/CHANGES/11673.bugfix.rst @@ -0,0 +1,2 @@ +Fixed routing to a sub-application added via ``.add_domain()`` not working +if the same path exists on the parent app. -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 124afbba929..2642ed6542e 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -977,6 +977,21 @@ async def resolve(self, request: Request) -> UrlMappingMatchInfo: resource_index = self._resource_index allowed_methods: set[str] = set() + # MatchedSubAppResource is primarily used to match on domain names + # (though custom rules could match on other things). This means that + # the traversal algorithm below can't be applied, and that we likely + # need to check these first so a sub app that defines the same path + # as a parent app will get priority if there's a domain match. + # + # For most cases we do not expect there to be many of these since + # currently they are only added by `.add_domain()`. + for resource in self._matched_sub_app_resources: + match_dict, allowed = await resource.resolve(request) + if match_dict is not None: + return match_dict + else: + allowed_methods |= allowed + # Walk the url parts looking for candidates. We walk the url backwards # to ensure the most explicit match is found first. If there are multiple # candidates for a given url part because there are multiple resources @@ -994,21 +1009,6 @@ async def resolve(self, request: Request) -> UrlMappingMatchInfo: break url_part = url_part.rpartition("/")[0] or "/" - # - # We didn't find any candidates, so we'll try the matched sub-app - # resources which we have to walk in a linear fashion because they - # have regex/wildcard match rules and we cannot index them. - # - # For most cases we do not expect there to be many of these since - # currently they are only added by `add_domain` - # - for resource in self._matched_sub_app_resources: - match_dict, allowed = await resource.resolve(request) - if match_dict is not None: - return match_dict - else: - allowed_methods |= allowed - if allowed_methods: return MatchInfoError(HTTPMethodNotAllowed(request.method, allowed_methods)) diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 25f45c38ab2..7347e5d9b06 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -1539,6 +1539,14 @@ Application and Router matches the pattern *domain* then further resolving is passed to *subapp*. + .. warning:: + + Registering many domains using this method may cause performance + issues with handler routing. If you have a substantial number of + applications for different domains, you may want to consider + using a reverse proxy (such as Nginx) to handle routing to + different apps, rather that registering them as sub-applications. + :param str domain: domain or mask of domain for the resource. :param Application subapp: nested application. diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py index 50b1ad2e9e2..d0541587e07 100644 --- a/tests/test_web_urldispatcher.py +++ b/tests/test_web_urldispatcher.py @@ -951,6 +951,28 @@ async def get(self) -> web.Response: assert r.status == 200 +async def test_subapp_domain_routing_same_path(aiohttp_client: AiohttpClient) -> None: + """Regression test for #11665.""" + app = web.Application() + sub_app = web.Application() + + async def mainapp_handler(request: web.Request) -> web.Response: + assert False + + async def subapp_handler(request: web.Request) -> web.Response: + return web.Response(text="SUBAPP") + + app.router.add_get("/", mainapp_handler) + sub_app.router.add_get("/", subapp_handler) + app.add_domain("different.example.com", sub_app) + + client = await aiohttp_client(app) + async with client.get("/", headers={"Host": "different.example.com"}) as r: + assert r.status == 200 + result = await r.text() + assert result == "SUBAPP" + + async def test_route_with_regex(aiohttp_client: AiohttpClient) -> None: """Test a route with a regex preceded by a fixed string.""" app = web.Application()