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