aio-libs / aio-libs/aiohttp

Dynamic routes with percent-encodable fixed segments (space, non-ASCII) are unreachable and url_for() URLs 404

Đang mở
#13,433 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
bug
Ngôn ngữ chính
Python
Star
16.5k
Fork
2.4k
Merge trung bình
17 giờ 22 phút
Pull request đã merge (30 ngày)
212

Mô tả

### Describe the bug

Dynamic routes (and static/prefix resources) whose fixed path segment contains characters that need percent-encoding — a space, non-ASCII, or some reserved characters — are unreachable: every request to them returns **404**, and the URL produced by `url_for()` cannot be routed back either.

The fixed part is percent-encoded once at registration (`_requote_path`) and the encoded form is used **both** for the resource-index key and the match pattern. But `UrlDispatcher.resolve` selects candidates by walking the **decoded** `request.rel_url.path_safe` backwards. The encoded index key is therefore never probed, so the resource is never a candidate.

Example: `app.router.add_get("/hello world/{name}", handler, name="greet")` compiles to formatter `/hello%20world/{name}` and index key `/hello%20world`. A request for `/hello%20world/john` decodes to `/hello world/john`, and the backward walk probes `/hello world/john`, `/hello world`, `/` — never `/hello%20world` — so it 404s. The same applies to `StaticResource` prefixes (`app.router.add_static("/static files/", ...)` is unreachable over `/static%20files/...`).

Verified on aiohttp 3.14.3 and on master.

Relevant code (aiohttp 3.14.3, `aiohttp/web_urldispatcher.py`):

- `DynamicResource.__init__` encodes fixed parts and uses the encoded form for both formatter and pattern literal: `part = _requote_path(part)` (:457) and `pattern += re.escape(part)` (:459); `self._formatter = formatter` (:468).
- `DynamicResource.canonical` returns that encoded formatter (:471-472).
- `_get_resource_index_key` derives the index key from the encoded canonical (:1110-1119): for `/hello%20world/{name}` the key is `/hello%20world`.
- `resolve` walks the **decoded** path backwards (`url_part = request.rel_url.path_safe` :1033; loop :1034-1043) and 404s when no candidate matches (:1048).

### To Reproduce

```python
import asyncio

from aiohttp import web

async def main() -> None:
app = web.Application()

# dynamic route with a percent-encodable fixed segment (space)
app.router.add_get("/hello world/{name}", lambda r: web.Response(text=r.match_info["name"]), name="greet")
# plain route with the same fixed segment (control: works)
app.router.add_get("/hello world/", lambda r: web.Response(text="plain"))

runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", 8080)
await site.start()

async def get(path: str) -> None:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:8080{path}") as resp:
print(f"GET {path:24} -> {resp.status}")

await get("/hello%20world/") # plain route -> 200
await get("/hello%20world/john") # dynamic route -> 404 (expected 200)
await get(str(app.router["greet"].url_for(name="john"))) # url_for -> 404

await runner.cleanup()

asyncio.run(main())
```

Output:

```console
GET /hello%20world/ -> 200
GET /hello%20world/john -> 404
GET /hello%20world/john -> 404
```

### Expected behavior

A dynamic route registered as `/hello world/{name}` must serve requests to the encoded URL `/hello%20world/{name}` (that is what `url_for()` produces), matching the decoded-path semantics the router uses everywhere else. The plain route with the same fixed segment resolves fine, so such characters are clearly supported — only the dynamic/static registration path is broken.

### Logs/tracebacks

```python-traceback
No traceback; the requests just return 404. Full self-checking repro with controls (also covers `StaticResource`):

PASS - c1: plain route '/hello world/' resolves over %20
PASS - c2: dynamic route without encodable chars resolves
FAIL - a3: StaticResource over '/static files/' resolves -> 200 (got 404)
FAIL - a1: dynamic '/hello world/{name}' matches -> 200 (got 404)
FAIL - a2: url_for-generated URL resolves back -> 200 (got 404)
```

### Python Version

```console
$ python --version
Python 3.10.12
```

### aiohttp Version

```console
$ python -m pip show aiohttp
Name: aiohttp
Version: 3.14.3 (also reproduced on master @ 356d355b7)
```

### multidict Version

```console
$ python -m pip show multidict
Version: 6.7.1
```

### propcache Version

```console
$ python -m pip show propcache
Version: 0.5.2
```

### yarl Version

```console
$ python -m pip show yarl
Version: 1.24.5
```

### OS

Linux

### Related component

Server

### Additional context

The mismatch is between the encoded canonical/index key (registration side) and the decoded `path_safe` traversal (resolution side). Direction for a fix: `resolve`/`_get_resource_index_key` must agree on one representation. Since the router already matches on the decoded path everywhere else (`Resource.resolve`, `StaticResource.resolve`, and the `path_safe` walk), the index key should be derived from the decoded path too (e.g. keep the raw fixed segment for the key), or the backward walk should also probe the encoded forms. `url_for`/resolve round-tripping is the documented contract (`docs/web_quickstart.rst`).

### Code of Conduct

- [x] I agree to follow the aio-libs Code of Conduct

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.