Repository: encode/httpx

Issue:
ASGI `raw_path` scope key should not include the query string portion.
I ran into a bug where I had written code that assumed `raw_path` as provided by Uvicorn would include the query string, where it turns out not to.

My tests didn't catch this because they were exercising the code using HTTPX ASGI emulation, and it turns out HTTPX thinks that `raw_path` DOES include the query string.

In Uvicorn: https://github.com/encode/uvicorn/blob/93bb8d3879808ae376b57e3721cc227fce2c27c1/uvicorn/protocols/http/h11_impl.py#L207
```python
                raw_path, _, query_string = event.target.partition(b"?")
```
But in HTTPX: https://github.com/encode/httpx/blob/9415af643f23600403740baad0a466edc5cdbec1/httpx/_urls.py#L277-L292

I'm pretty confident HTTPX is incorrect about this. The ASGI spec (coincidentally the one bit of it I contributed directly to) says: https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope

> `raw_path` (*byte string*) -- The original HTTP path component unmodified from the bytes that were received by the web server.

On reading it now I realize this is a little ambiguous.

Daphne (the closest we have to a reference implementation of ASGI) backs up the idea that `raw_path` and `path` should be almost identical except for their encoding:  https://github.com/django/daphne/blob/e49c39a4e5fac8ec170dd653641a9e90844fd3f1/daphne/ws_protocol.py#L77C1-L78

```python
                    "path": unquote(self.path.decode("ascii")),
                    "raw_path": self.path,
```


Discussion:
It looks like `url.raw_path` in HTTPX has been designed as the bytes including the query string.

Which means the bug is here: https://github.com/encode/httpx/blob/9415af643f23600403740baad0a466edc5cdbec1/httpx/_transports/asgi.py#L97-L111

That's where the `raw_path` ASGI scope is copied directly from `request.url.raw_path` - when actually it should only be the part of that up to the first `?`.
Accidentally closed this, reopening.
Hmm, I am not entirely sure how we should resolve this - the web server receives the query string bytes in the same part of the HTTP protocol as the path. If two servers agree, though, that's probably the best way.
Okay so there's two different aspects here...

There's the *ASGI scope key `"raw_path"`* which we use for our `ASGITransport`, here... https://github.com/encode/httpx/blob/e99e2948e64fac2ca498865e9742ff50a69a2155/httpx/_transports/asgi.py#L97-L111

Which, appears to be incorrect.

Then there's the `URL.raw_path` property. The issue with the property isn't that it's incorrect. It's that it's poorly named. It probably ought to be `URL.target`. Which is a useful and as-good-as-we-can-manage-to-name-it property representing [the (origin form of the) HTTP/1.1 request-target](https://datatracker.ietf.org/doc/html/rfc7230#section-5.3). Or in other words, the actual bytes-on-the-wire that get sent in the leading line of an outgoing HTTP/1.1 request.

```
GET /hello.txt?test HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
```

See also https://h11.readthedocs.io/en/latest/api.html#h11.Request

Let's treat this issue as *just* addressing the ASGI `raw_path` scope variable, and resolve that here...

https://github.com/encode/httpx/blob/e99e2948e64fac2ca498865e9742ff50a69a2155/httpx/_transports/asgi.py#L106

*(Incidentally I think if we'd been really sharp we might have clocked onto this back in https://github.com/django/asgiref/issues/87 and gone with a `target` instead of `raw_path` there too. Better mapping to whats on the wire, and also allows application code to differentiate between `/example` and `/example?`)*
Yeah, I agree it was poorly named - I think I leaned too close to WSGI on this one. Unfortunately, we now have what we have.

Resolving patch:
diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py
--- a/httpx/_transports/asgi.py
+++ b/httpx/_transports/asgi.py
@@ -103,7 +103,7 @@ async def handle_async_request(
             "headers": [(k.lower(), v) for (k, v) in request.headers.raw],
             "scheme": request.url.scheme,
             "path": request.url.path,
-            "raw_path": request.url.raw_path,
+            "raw_path": request.url.raw_path.split(b"?")[0],
             "query_string": request.url.query,
             "server": (request.url.host, request.url.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
@@ -120,6 +120,19 @@ async def test_asgi_raw_path():
     assert response.json() == {"raw_path": "/user@example.org"}
 
 
+@pytest.mark.anyio
+async def test_asgi_raw_path_should_not_include_querystring_portion():
+    """
+    See https://github.com/encode/httpx/issues/2810
+    """
+    async with httpx.AsyncClient(app=echo_raw_path) as client:
+        url = httpx.URL("http://www.example.org/path?query")
+        response = await client.get(url)
+
+    assert response.status_code == 200
+    assert response.json() == {"raw_path": "/path"}
+
+
 @pytest.mark.anyio
 async def test_asgi_upload():
     async with httpx.AsyncClient(app=echo_body) as client: