From 80d21eefe5bcd7f5e8ee333e7ec043dec5e168bd Mon Sep 17 00:00:00 2001 From: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:07:30 +0400 Subject: [PATCH] Add `target` request extension (#888) * Add target request extension * Add changelog * Implement target in the models.py, add test * Update docs/extensions.md * Update extensions.md --------- Co-authored-by: Tom Christie <tom.christie@krakentechnologies.ltd> Co-authored-by: Tom Christie <tom@tomchristie.com> --- CHANGELOG.md | 3 ++- docs/extensions.md | 28 +++++++++++++++++++++++++--- httpcore/_models.py | 8 ++++++++ tests/test_models.py | 14 ++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c66d6a1..5ad7dc42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased -- Fix support for connection Upgrade and CONNECT when some data in the stream has been read. (#882) +- Add `target` request extension. (#888) +- Fix support for connection `Upgrade` and `CONNECT` when some data in the stream has been read. (#882) ## 1.0.3 (February 13th, 2024) diff --git a/docs/extensions.md b/docs/extensions.md index 2bec844e..7a24a418 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -166,6 +166,28 @@ response = httpcore.request( ) ``` +### `"target"` + +The target that is used as [the HTTP target instead of the URL path](https://datatracker.ietf.org/doc/html/rfc2616#section-5.1.2). + +This enables support constructing requests that would otherwise be unsupported. In particular... + +* Forward proxy requests using an absolute URI. +* Tunneling proxy requests using `CONNECT` with hostname as the target. +* Server-wide `OPTIONS *` requests. + +For example: + +```python +extensions = {"target": b"www.encode.io:443"} +response = httpcore.request( + "CONNECT", + "http://your-tunnel-proxy.com", + headers=headers, + extensions=extensions +) +``` + ## Response Extensions ### `"http_version"` @@ -214,9 +236,9 @@ A proxy CONNECT request using the network stream: # This will establish a connection to 127.0.0.1:8080, and then send the following... # # CONNECT http://www.example.com HTTP/1.1 -# Host: 127.0.0.1:8080 -url = httpcore.URL(b"http", b"127.0.0.1", 8080, b"http://www.example.com") -with httpcore.stream("CONNECT", url) as response: +url = "http://127.0.0.1:8080" +extensions = {"target: "http://www.example.com"} +with httpcore.stream("CONNECT", url, extensions=extensions) as response: network_stream = response.extensions["network_stream"] # Upgrade to an SSL stream... diff --git a/httpcore/_models.py b/httpcore/_models.py index 11bfcd84..397bd758 100644 --- a/httpcore/_models.py +++ b/httpcore/_models.py @@ -353,6 +353,14 @@ def __init__( ) self.extensions = {} if extensions is None else extensions + if "target" in self.extensions: + self.url = URL( + scheme=self.url.scheme, + host=self.url.host, + port=self.url.port, + target=self.extensions["target"], + ) + def __repr__(self) -> str: return f"<{self.__class__.__name__} [{self.method!r}]>" diff --git a/tests/test_models.py b/tests/test_models.py index 104da310..35b6e947 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -58,6 +58,20 @@ def test_request(): assert repr(request.stream) == "<ByteStream [0 bytes]>" +def test_request_with_target_extension(): + extensions = {"target": b"/another_path"} + request = httpcore.Request( + "GET", "https://www.example.com/path", extensions=extensions + ) + assert request.url.target == b"/another_path" + + extensions = {"target": b"/unescaped|path"} + request = httpcore.Request( + "GET", "https://www.example.com/path", extensions=extensions + ) + assert request.url.target == b"/unescaped|path" + + def test_request_with_invalid_method(): with pytest.raises(TypeError) as exc_info: httpcore.Request(123, "https://www.example.com/") # type: ignore