Repository: encode/httpx

Issue:
ASGITransport does not correctly simulate raw_path in the scope
I'm trying to switch Datasette's internal tests over to using `httpx` with `AsyncClient`.

This has almost worked perfectly, but I've run into one problem: it looks like the `ASGITransport` class used by the `AsyncClient(app=asgi_app)` mechanism does not correctly simulate the `raw_path` and `path` keys.

Here's the code in question: https://github.com/encode/httpx/blob/92ca4d0cc654859fc2257c492e55d8752370d427/httpx/_transports/asgi.py#L82-L97

As you can see, it's not populating `raw_path` even though that's part of the ASGI spec.

This matters for Datasette because it supports this URL: https://latest.datasette.io/fixtures/table%2Fwith%2Fslashes.csv - which refers to a SQLite database table called `table/with/slashes.csv` (a weird table name but that's test cases for you). The way it does this is through careful decoding of the `raw_path` ASGI scope variable.

Here are my notes when I first ran into this limitation of ASGITransport: https://github.com/simonw/datasette/pull/1000#issuecomment-705945591
ASGITransport does not correctly simulate raw_path in the scope
I'm trying to switch Datasette's internal tests over to using `httpx` with `AsyncClient`.

This has almost worked perfectly, but I've run into one problem: it looks like the `ASGITransport` class used by the `AsyncClient(app=asgi_app)` mechanism does not correctly simulate the `raw_path` and `path` keys.

Here's the code in question: https://github.com/encode/httpx/blob/92ca4d0cc654859fc2257c492e55d8752370d427/httpx/_transports/asgi.py#L82-L97

As you can see, it's not populating `raw_path` even though that's part of the ASGI spec.

This matters for Datasette because it supports this URL: https://latest.datasette.io/fixtures/table%2Fwith%2Fslashes.csv - which refers to a SQLite database table called `table/with/slashes.csv` (a weird table name but that's test cases for you). The way it does this is through careful decoding of the `raw_path` ASGI scope variable.

Here are my notes when I first ran into this limitation of ASGITransport: https://github.com/simonw/datasette/pull/1000#issuecomment-705945591


Discussion:
The fix here may be as straight-forward as adding this line to the code that constructs the scope:

```python
    "raw_path": path,
```
The fix here may be as straight-forward as adding this line to the code that constructs the scope:

```python
    "raw_path": path,
```

Resolving patch:
diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py
--- a/httpx/_transports/asgi.py
+++ b/httpx/_transports/asgi.py
@@ -90,6 +90,7 @@ async def arequest(
             "headers": [(k.lower(), v) for (k, v) in headers],
             "scheme": scheme.decode("ascii"),
             "path": unquote(path.decode("ascii")),
+            "raw_path": path,
             "query_string": query,
             "server": (host.decode("ascii"), port),
             "client": self.client,


Test patch:
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -23,6 +23,15 @@ async def echo_path(scope, receive, send):
     await send({"type": "http.response.body", "body": output})
 
 
+async def echo_raw_path(scope, receive, send):
+    status = 200
+    output = json.dumps({"raw_path": scope["raw_path"].decode("ascii")}).encode("utf-8")
+    headers = [(b"content-type", "text/plain"), (b"content-length", str(len(output)))]
+
+    await send({"type": "http.response.start", "status": status, "headers": headers})
+    await send({"type": "http.response.body", "body": output})
+
+
 async def echo_body(scope, receive, send):
     status = 200
     headers = [(b"content-type", "text/plain")]
@@ -80,6 +89,16 @@ async def test_asgi_urlencoded_path():
     assert response.json() == {"path": "/user@example.org"}
 
 
+@pytest.mark.usefixtures("async_environment")
+async def test_asgi_raw_path():
+    async with httpx.AsyncClient(app=echo_raw_path) as client:
+        url = httpx.URL("http://www.example.org/").copy_with(path="/user@example.org")
+        response = await client.get(url)
+
+    assert response.status_code == 200
+    assert response.json() == {"raw_path": "/user%40example.org"}
+
+
 @pytest.mark.usefixtures("async_environment")
 async def test_asgi_upload():
     async with httpx.AsyncClient(app=echo_body) as client: