Skip to content

Commit

Permalink
Fix router matching pre-encoded URLs (#8898)
Browse files Browse the repository at this point in the history
Co-authored-by: J. Nick Koston <nick@koston.org>
(cherry picked from commit 6be9452)
  • Loading branch information
Dreamsorcerer committed Aug 31, 2024
1 parent 7477503 commit 2a40ba4
Show file tree
Hide file tree
Showing 9 changed files with 32 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGES/8898.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed web router not matching pre-encoded URLs (requires yarl 1.9.6+) -- by :user:`Dreamsorcerer`.
10 changes: 5 additions & 5 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ def register_route(self, route: "ResourceRoute") -> None:
async def resolve(self, request: Request) -> _Resolve:
allowed_methods: Set[str] = set()

match_dict = self._match(request.rel_url.raw_path)
match_dict = self._match(request.rel_url.path)
if match_dict is None:
return None, allowed_methods

Expand Down Expand Up @@ -650,7 +650,7 @@ def set_options_route(self, handler: Handler) -> None:
)

async def resolve(self, request: Request) -> _Resolve:
path = request.rel_url.raw_path
path = request.rel_url.path
method = request.method
allowed_methods = set(self._routes)
if not path.startswith(self._prefix2) and path != self._prefix:
Expand Down Expand Up @@ -1040,7 +1040,7 @@ async def resolve(self, request: Request) -> UrlMappingMatchInfo:
# candidates for a given url part because there are multiple resources
# registered for the same canonical path, we resolve them in a linear
# fashion to ensure registration order is respected.
url_part = request.rel_url.raw_path
url_part = request.rel_url.path
while url_part:
for candidate in resource_index.get(url_part, ()):
match_dict, allowed = await candidate.resolve(request)
Expand Down Expand Up @@ -1165,7 +1165,7 @@ def add_resource(self, path: str, *, name: Optional[str] = None) -> Resource:
if resource.name == name and resource.raw_match(path):
return cast(Resource, resource)
if not ("{" in path or "}" in path or ROUTE_RE.search(path)):
resource = PlainResource(_requote_path(path), name=name)
resource = PlainResource(path, name=name)
self.register_resource(resource)
return resource
resource = DynamicResource(path, name=name)
Expand Down Expand Up @@ -1292,7 +1292,7 @@ def _quote_path(value: str) -> str:


def _unquote_path(value: str) -> str:
return URL.build(path=value, encoded=True).path
return URL.build(path=value, encoded=True).path.replace("%2F", "/")


def _requote_path(value: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ pycparser==2.22
# via cffi
uvloop==0.20.0 ; platform_system != "Windows" and implementation_name == "cpython"
# via -r requirements/base.in
yarl==1.9.4
yarl==1.9.6
# via -r requirements/runtime-deps.in
2 changes: 1 addition & 1 deletion requirements/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ webcolors==24.8.0
# via blockdiag
wheel==0.44.0
# via pip-tools
yarl==1.9.4
yarl==1.9.6
# via -r requirements/runtime-deps.in
zipp==3.20.1
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ webcolors==24.8.0
# via blockdiag
wheel==0.44.0
# via pip-tools
yarl==1.9.4
yarl==1.9.6
# via -r requirements/runtime-deps.in
zipp==3.20.1
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/runtime-deps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ pycares==4.4.0
# via aiodns
pycparser==2.22
# via cffi
yarl==1.9.4
yarl==1.9.6
# via -r requirements/runtime-deps.in
2 changes: 1 addition & 1 deletion requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,5 @@ uvloop==0.20.0 ; platform_system != "Windows" and implementation_name == "cpytho
# via -r requirements/base.in
wait-for-it==2.2.2
# via -r requirements/test.in
yarl==1.9.4
yarl==1.9.6
# via -r requirements/runtime-deps.in
29 changes: 17 additions & 12 deletions tests/test_urldispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ def test_add_static_quoting(router) -> None:
)
assert router["static"] is resource
url = resource.url_for(filename="/1 2/файл%2F.txt")
assert url.path == "/пре /фикс/1 2/файл%2F.txt"
assert url.path == "/пре %2Fфикс/1 2/файл%2F.txt"
assert str(url) == (
"/%D0%BF%D1%80%D0%B5%20%2F%D1%84%D0%B8%D0%BA%D1%81"
"/1%202/%D1%84%D0%B0%D0%B9%D0%BB%252F.txt"
Expand Down Expand Up @@ -530,19 +530,24 @@ def test_static_remove_trailing_slash(router) -> None:
assert "/prefix" == route._prefix


async def test_add_route_with_re(router) -> None:
@pytest.mark.parametrize(
"pattern,url,expected",
(
(r"{to:\d+}", r"1234", {"to": "1234"}),
("{name}.html", "test.html", {"name": "test"}),
(r"{fn:\w+ \d+}", "abc 123", {"fn": "abc 123"}),
(r"{fn:\w+\s\d+}", "abc 123", {"fn": "abc 123"}),
),
)
async def test_add_route_with_re(
router: web.UrlDispatcher, pattern: str, url: str, expected
) -> None:
handler = make_handler()
router.add_route("GET", r"/handler/{to:\d+}", handler)

req = make_mocked_request("GET", "/handler/1234")
router.add_route("GET", f"/handler/{pattern}", handler)
req = make_mocked_request("GET", f"/handler/{url}")
info = await router.resolve(req)
assert info is not None
assert {"to": "1234"} == info

router.add_route("GET", r"/handler/{name}.html", handler)
req = make_mocked_request("GET", "/handler/test.html")
info = await router.resolve(req)
assert {"name": "test"} == info
assert info == expected


async def test_add_route_with_re_and_slashes(router) -> None:
Expand Down Expand Up @@ -625,7 +630,7 @@ def test_route_dynamic_quoting(router) -> None:
route = router.add_route("GET", r"/пре %2Fфикс/{arg}", handler)

url = route.url_for(arg="1 2/текст%2F")
assert url.path == "/пре /фикс/1 2/текст%2F"
assert url.path == "/пре %2Fфикс/1 2/текст%2F"
assert str(url) == (
"/%D0%BF%D1%80%D0%B5%20%2F%D1%84%D0%B8%D0%BA%D1%81"
"/1%202/%D1%82%D0%B5%D0%BA%D1%81%D1%82%252F"
Expand Down
12 changes: 4 additions & 8 deletions tests/test_web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,18 +856,15 @@ async def get_foobar(request: web.Request) -> web.Response:
assert (await resp.text()) == "success!"


@pytest.mark.xfail(
raises=AssertionError,
reason="Regression in v3.7: https://github.com/aio-libs/aiohttp/issues/5621",
)
@pytest.mark.parametrize(
("route_definition", "urlencoded_path", "expected_http_resp_status"),
(
("/467,802,24834/hello", "/467%2C802%2C24834/hello", 200),
("/{user_ids:([0-9]+)(,([0-9]+))*}/hello", "/467%2C802%2C24834/hello", 200),
("/467,802,24834/hello", "/467,802,24834/hello", 200),
("/{user_ids:([0-9]+)(,([0-9]+))*}/hello", "/467,802,24834/hello", 200),
("/1%2C3/hello", "/1%2C3/hello", 404),
),
ids=("urldecoded_route", "urldecoded_route_with_regex", "urlencoded_route"),
)
async def test_decoded_url_match(
aiohttp_client: AiohttpClient,
Expand All @@ -883,9 +880,8 @@ async def handler(request: web.Request) -> web.Response:
app.router.add_get(route_definition, handler)
client = await aiohttp_client(app)

r = await client.get(yarl.URL(urlencoded_path, encoded=True))
assert r.status == expected_http_resp_status
await r.release()
async with client.get(yarl.URL(urlencoded_path, encoded=True)) as resp:
assert resp.status == expected_http_resp_status


async def test_order_is_preserved(aiohttp_client: AiohttpClient) -> None:
Expand Down

0 comments on commit 2a40ba4

Please sign in to comment.