From 1590c86059f20d7955e523de139d35f88e87eb23 Mon Sep 17 00:00:00 2001 From: Alastair <22815637+alastairvox@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:42:22 -0600 Subject: [PATCH 1/2] Add xfail test for #11665 (#11666) --- tests/test_web_urldispatcher.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py index 50b1ad2e9e2..d98c41dc35f 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 +@pytest.mark.xfail(reason="https://github.com/aio-libs/aiohttp/issues/11665") +async def test_subapp_domain_routing_same_path(aiohttp_client: AiohttpClient) -> None: + 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() From e8c5252cb28ce1db0232fc5fc66909791313c08b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Thu, 16 Oct 2025 17:50:40 +0100 Subject: [PATCH 2/2] Fix domain matching being lower priority than path matching (#11673) --- CHANGES/11673.bugfix.rst | 2 ++ aiohttp/web_urldispatcher.py | 30 +++++++++++++++--------------- docs/web_reference.rst | 8 ++++++++ tests/test_web_urldispatcher.py | 2 +- 4 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 CHANGES/11673.bugfix.rst 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 d98c41dc35f..d0541587e07 100644 --- a/tests/test_web_urldispatcher.py +++ b/tests/test_web_urldispatcher.py @@ -951,8 +951,8 @@ async def get(self) -> web.Response: assert r.status == 200 -@pytest.mark.xfail(reason="https://github.com/aio-libs/aiohttp/issues/11665") async def test_subapp_domain_routing_same_path(aiohttp_client: AiohttpClient) -> None: + """Regression test for #11665.""" app = web.Application() sub_app = web.Application()