Repository: encode/httpcore

Issue:
Add proxy configuration to `httpcore.ConnectionPool`
Proxy configuration is currently supported through two connection pool subclasses...

* `SyncHTTPProxy(...)`
* `SyncSOCKSProxy(...)`

We could instead have proxy configuration on the main class...

```python
pool = httpcore.SyncConnectionPool(
    ssl_context=ssl_context,
    proxy=httpcore.Proxy(url, auth, headers, ssl_context),
    ...
)
```

With the switching handled in `create_connection`.

Benefits...

* Neater API, with fewer types exposed.
* Opens a pathway to handling system proxies within `httpcore` rather than at the layer above.

<sup>* Let's also have a `httpcore.Proxy()` class here to encapsulate the configuration.</sup>

Discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f63110425..e33b10c40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## [Unreleased]
+
+- Support `proxy=…` configuration on `ConnectionPool()`.
+
 ## Version 1.0.6 (October 1st, 2024)
 
 - Relax `trio` dependency pinning. (#956)
diff --git a/docs/async.md b/docs/async.md
index f80f8b7d6..e0f0a65c8 100644
--- a/docs/async.md
+++ b/docs/async.md
@@ -34,14 +34,6 @@ async with httpcore.AsyncConnectionPool() as http:
     ...
 ```
 
-Or if connecting via a proxy:
-
-```python
-# The async variation of `httpcore.HTTPProxy`
-async with httpcore.AsyncHTTPProxy() as proxy:
-    ...
-```
-
 ### Sending requests
 
 Sending requests with the async version of `httpcore` requires the `await` keyword:
@@ -221,10 +213,3 @@ anyio.run(main)
     handler: python
     rendering:
         show_source: False
-
-## `httpcore.AsyncHTTPProxy`
-
-::: httpcore.AsyncHTTPProxy
-    handler: python
-    rendering:
-        show_source: False
diff --git a/docs/proxies.md b/docs/proxies.md
index 72eaeb644..970d53d53 100644
--- a/docs/proxies.md
+++ b/docs/proxies.md
@@ -7,7 +7,8 @@ Sending requests via a proxy is very similar to sending requests using a standar
 ```python
 import httpcore
 
-proxy = httpcore.HTTPProxy(proxy_url="http://127.0.0.1:8080/")
+proxy = httpcore.Proxy("http://127.0.0.1:8080/")
+pool = httpcore.ConnectionPool(proxy=proxy)
 r = proxy.request("GET", "https://www.example.com/")
 
 print(r)
@@ -31,10 +32,11 @@ Proxy authentication can be included in the initial configuration:
 import httpcore
 
 # A `Proxy-Authorization` header will be included on the initial proxy connection.
-proxy = httpcore.HTTPProxy(
-    proxy_url="http://127.0.0.1:8080/",
-    proxy_auth=("<username>", "<password>")
+proxy = httpcore.Proxy(
+    url="http://127.0.0.1:8080/",
+    auth=("<username>", "<password>")
 )
+pool = httpcore.ConnectionPool(proxy=proxy)
 ```
 
 Custom headers can also be included:
@@ -45,10 +47,11 @@ import base64
 
 # Construct and include a `Proxy-Authorization` header.
 auth = base64.b64encode(b"<username>:<password>")
-proxy = httpcore.HTTPProxy(
-    proxy_url="http://127.0.0.1:8080/",
-    proxy_headers={"Proxy-Authorization": b"Basic " + auth}
+proxy = httpcore.Proxy(
+    url="http://127.0.0.1:8080/",
+    headers={"Proxy-Authorization": b"Basic " + auth}
 )
+pool = httpcore.ConnectionPool(proxy=proxy)
 ```
 
 ## Proxy SSL
@@ -58,10 +61,10 @@ The `httpcore` package also supports HTTPS proxies for http and https destinatio
 HTTPS proxies can be used in the same way that HTTP proxies are.
 
 ```python
-proxy = httpcore.HTTPProxy(proxy_url="https://127.0.0.1:8080/")
+proxy = httpcore.Proxy(url="https://127.0.0.1:8080/")
 ```
 
-Also, when using HTTPS proxies, you may need to configure the SSL context, which you can do with the `proxy_ssl_context` argument.
+Also, when using HTTPS proxies, you may need to configure the SSL context, which you can do with the `ssl_context` argument.
 
 ```python
 import ssl
@@ -70,11 +73,13 @@ import httpcore
 proxy_ssl_context = ssl.create_default_context()
 proxy_ssl_context.check_hostname = False
 
-proxy = httpcore.HTTPProxy('https://127.0.0.1:8080/', proxy_ssl_context=proxy_ssl_context)
+proxy = httpcore.Proxy(
+    url='https://127.0.0.1:8080/',
+    ssl_context=proxy_ssl_context
+)
+pool = httpcore.ConnectionPool(proxy=proxy)
 ```
 
-It is important to note that the `ssl_context` argument is always used for the remote connection, and the `proxy_ssl_context` argument is always used for the proxy connection.
-
 ## HTTP Versions
 
 If you use proxies, keep in mind that the `httpcore` package only supports proxies to HTTP/1.1 servers.
@@ -91,8 +96,9 @@ The `SOCKSProxy` class should be using instead of a standard connection pool:
 import httpcore
 
 # Note that the SOCKS port is 1080.
-proxy = httpcore.SOCKSProxy(proxy_url="socks5://127.0.0.1:1080/")
-r = proxy.request("GET", "https://www.example.com/")
+proxy = httpcore.Proxy(url="socks5://127.0.0.1:1080/")
+pool = httpcore.ConnectionPool(proxy=proxy)
+r = pool.request("GET", "https://www.example.com/")
 ```
 
 Authentication via SOCKS is also supported:
@@ -100,20 +106,21 @@ Authentication via SOCKS is also supported:
 ```python
 import httpcore
 
-proxy = httpcore.SOCKSProxy(
-    proxy_url="socks5://127.0.0.1:8080/",
-    proxy_auth=("<username>", "<password>")
+proxy = httpcore.Proxy(
+    url="socks5://127.0.0.1:1080/",
+    auth=("<username>", "<password>"),
 )
-r = proxy.request("GET", "https://www.example.com/")
+pool = httpcore.ConnectionPool(proxy=proxy)
+r = pool.request("GET", "https://www.example.com/")
 ```
 
 ---
 
 # Reference
 
-## `httpcore.HTTPProxy`
+## `httpcore.Proxy`
 
-::: httpcore.HTTPProxy
+::: httpcore.Proxy
     handler: python
     rendering:
         show_source: False
diff --git a/docs/table-of-contents.md b/docs/table-of-contents.md
index 3cf1f725e..5dc9a10b4 100644
--- a/docs/table-of-contents.md
+++ b/docs/table-of-contents.md
@@ -10,14 +10,13 @@
 * Connection Pools
     * `httpcore.ConnectionPool`
 * Proxies
-    * `httpcore.HTTPProxy`
+    * `httpcore.Proxy`
 * Connections
     * `httpcore.HTTPConnection`
     * `httpcore.HTTP11Connection`
     * `httpcore.HTTP2Connection`
 * Async Support
     * `httpcore.AsyncConnectionPool`
-    * `httpcore.AsyncHTTPProxy`
     * `httpcore.AsyncHTTPConnection`
     * `httpcore.AsyncHTTP11Connection`
     * `httpcore.AsyncHTTP2Connection`
diff --git a/httpcore/__init__.py b/httpcore/__init__.py
index 330745a5d..0d4946e70 100644
--- a/httpcore/__init__.py
+++ b/httpcore/__init__.py
@@ -34,7 +34,7 @@
     WriteError,
     WriteTimeout,
 )
-from ._models import URL, Origin, Request, Response
+from ._models import URL, Origin, Proxy, Request, Response
 from ._ssl import default_ssl_context
 from ._sync import (
     ConnectionInterface,
@@ -79,6 +79,7 @@ def __init__(self, *args, **kwargs):  # type: ignore
     "URL",
     "Request",
     "Response",
+    "Proxy",
     # async
     "AsyncHTTPConnection",
     "AsyncConnectionPool",
diff --git a/httpcore/_async/connection_pool.py b/httpcore/_async/connection_pool.py
index 0795b9ccb..96e973d0c 100644
--- a/httpcore/_async/connection_pool.py
+++ b/httpcore/_async/connection_pool.py
@@ -8,7 +8,7 @@
 from .._backends.auto import AutoBackend
 from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
 from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
-from .._models import Origin, Request, Response
+from .._models import Origin, Proxy, Request, Response
 from .._synchronization import AsyncEvent, AsyncShieldCancellation, AsyncThreadLock
 from .connection import AsyncHTTPConnection
 from .interfaces import AsyncConnectionInterface, AsyncRequestInterface
@@ -48,6 +48,7 @@ class AsyncConnectionPool(AsyncRequestInterface):
     def __init__(
         self,
         ssl_context: ssl.SSLContext | None = None,
+        proxy: Proxy | None = None,
         max_connections: int | None = 10,
         max_keepalive_connections: int | None = None,
         keepalive_expiry: float | None = None,
@@ -89,7 +90,7 @@ def __init__(
              in the TCP socket when the connection was established.
         """
         self._ssl_context = ssl_context
-
+        self._proxy = proxy
         self._max_connections = (
             sys.maxsize if max_connections is None else max_connections
         )
@@ -125,6 +126,45 @@ def __init__(
         self._optional_thread_lock = AsyncThreadLock()
 
     def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
+        if self._proxy is not None:
+            if self._proxy.url.scheme in (b"socks5", b"socks5h"):
+                from .socks_proxy import AsyncSocks5Connection
+
+                return AsyncSocks5Connection(
+                    proxy_origin=self._proxy.url.origin,
+                    proxy_auth=self._proxy.auth,
+                    remote_origin=origin,
+                    ssl_context=self._ssl_context,
+                    keepalive_expiry=self._keepalive_expiry,
+                    http1=self._http1,
+                    http2=self._http2,
+                    network_backend=self._network_backend,
+                )
+            elif origin.scheme == b"http":
+                from .http_proxy import AsyncForwardHTTPConnection
+
+                return AsyncForwardHTTPConnection(
+                    proxy_origin=self._proxy.url.origin,
+                    proxy_headers=self._proxy.headers,
+                    proxy_ssl_context=self._proxy.ssl_context,
+                    remote_origin=origin,
+                    keepalive_expiry=self._keepalive_expiry,
+                    network_backend=self._network_backend,
+                )
+            from .http_proxy import AsyncTunnelHTTPConnection
+
+            return AsyncTunnelHTTPConnection(
+                proxy_origin=self._proxy.url.origin,
+                proxy_headers=self._proxy.headers,
+                proxy_ssl_context=self._proxy.ssl_context,
+                remote_origin=origin,
+                ssl_context=self._ssl_context,
+                keepalive_expiry=self._keepalive_expiry,
+                http1=self._http1,
+                http2=self._http2,
+                network_backend=self._network_backend,
+            )
+
         return AsyncHTTPConnection(
             origin=origin,
             ssl_context=self._ssl_context,
diff --git a/httpcore/_async/http_proxy.py b/httpcore/_async/http_proxy.py
index 4cbfe5186..cc9d92066 100644
--- a/httpcore/_async/http_proxy.py
+++ b/httpcore/_async/http_proxy.py
@@ -51,12 +51,7 @@ def merge_headers(
     return default_headers + override_headers
 
 
-def build_auth_header(username: bytes, password: bytes) -> bytes:
-    userpass = username + b":" + password
-    return b"Basic " + base64.b64encode(userpass)
-
-
-class AsyncHTTPProxy(AsyncConnectionPool):
+class AsyncHTTPProxy(AsyncConnectionPool):  # pragma: nocover
     """
     A connection pool that sends requests via an HTTP proxy.
     """
@@ -142,7 +137,8 @@ def __init__(
         if proxy_auth is not None:
             username = enforce_bytes(proxy_auth[0], name="proxy_auth")
             password = enforce_bytes(proxy_auth[1], name="proxy_auth")
-            authorization = build_auth_header(username, password)
+            userpass = username + b":" + password
+            authorization = b"Basic " + base64.b64encode(userpass)
             self._proxy_headers = [
                 (b"Proxy-Authorization", authorization)
             ] + self._proxy_headers
diff --git a/httpcore/_async/socks_proxy.py b/httpcore/_async/socks_proxy.py
index ef96a8c17..b363f55a0 100644
--- a/httpcore/_async/socks_proxy.py
+++ b/httpcore/_async/socks_proxy.py
@@ -102,7 +102,7 @@ async def _init_socks5_connection(
         raise ProxyError(f"Proxy Server could not connect: {reply_code}.")
 
 
-class AsyncSOCKSProxy(AsyncConnectionPool):
+class AsyncSOCKSProxy(AsyncConnectionPool):  # pragma: nocover
     """
     A connection pool that sends requests via an HTTP proxy.
     """
diff --git a/httpcore/_models.py b/httpcore/_models.py
index c739a7fa6..8a65f1334 100644
--- a/httpcore/_models.py
+++ b/httpcore/_models.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import base64
+import ssl
 import typing
 import urllib.parse
 
@@ -489,3 +491,26 @@ async def aclose(self) -> None:
             )
         if hasattr(self.stream, "aclose"):
             await self.stream.aclose()
+
+
+class Proxy:
+    def __init__(
+        self,
+        url: URL | bytes | str,
+        auth: tuple[bytes | str, bytes | str] | None = None,
+        headers: HeadersAsMapping | HeadersAsSequence | None = None,
+        ssl_context: ssl.SSLContext | None = None,
+    ):
+        self.url = enforce_url(url, name="url")
+        self.headers = enforce_headers(headers, name="headers")
+        self.ssl_context = ssl_context
+
+        if auth is not None:
+            username = enforce_bytes(auth[0], name="auth")
+            password = enforce_bytes(auth[1], name="auth")
+            userpass = username + b":" + password
+            authorization = b"Basic " + base64.b64encode(userpass)
+            self.auth: tuple[bytes, bytes] | None = (username, password)
+            self.headers = [(b"Proxy-Authorization", authorization)] + self.headers
+        else:
+            self.auth = None
diff --git a/httpcore/_sync/connection_pool.py b/httpcore/_sync/connection_pool.py
index 00c3983dd..9ccfa53e5 100644
--- a/httpcore/_sync/connection_pool.py
+++ b/httpcore/_sync/connection_pool.py
@@ -8,7 +8,7 @@
 from .._backends.sync import SyncBackend
 from .._backends.base import SOCKET_OPTION, NetworkBackend
 from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
-from .._models import Origin, Request, Response
+from .._models import Origin, Proxy, Request, Response
 from .._synchronization import Event, ShieldCancellation, ThreadLock
 from .connection import HTTPConnection
 from .interfaces import ConnectionInterface, RequestInterface
@@ -48,6 +48,7 @@ class ConnectionPool(RequestInterface):
     def __init__(
         self,
         ssl_context: ssl.SSLContext | None = None,
+        proxy: Proxy | None = None,
         max_connections: int | None = 10,
         max_keepalive_connections: int | None = None,
         keepalive_expiry: float | None = None,
@@ -89,7 +90,7 @@ def __init__(
              in the TCP socket when the connection was established.
         """
         self._ssl_context = ssl_context
-
+        self._proxy = proxy
         self._max_connections = (
             sys.maxsize if max_connections is None else max_connections
         )
@@ -125,6 +126,45 @@ def __init__(
         self._optional_thread_lock = ThreadLock()
 
     def create_connection(self, origin: Origin) -> ConnectionInterface:
+        if self._proxy is not None:
+            if self._proxy.url.scheme in (b"socks5", b"socks5h"):
+                from .socks_proxy import Socks5Connection
+
+                return Socks5Connection(
+                    proxy_origin=self._proxy.url.origin,
+                    proxy_auth=self._proxy.auth,
+                    remote_origin=origin,
+                    ssl_context=self._ssl_context,
+                    keepalive_expiry=self._keepalive_expiry,
+                    http1=self._http1,
+                    http2=self._http2,
+                    network_backend=self._network_backend,
+                )
+            elif origin.scheme == b"http":
+                from .http_proxy import ForwardHTTPConnection
+
+                return ForwardHTTPConnection(
+                    proxy_origin=self._proxy.url.origin,
+                    proxy_headers=self._proxy.headers,
+                    proxy_ssl_context=self._proxy.ssl_context,
+                    remote_origin=origin,
+                    keepalive_expiry=self._keepalive_expiry,
+                    network_backend=self._network_backend,
+                )
+            from .http_proxy import TunnelHTTPConnection
+
+            return TunnelHTTPConnection(
+                proxy_origin=self._proxy.url.origin,
+                proxy_headers=self._proxy.headers,
+                proxy_ssl_context=self._proxy.ssl_context,
+                remote_origin=origin,
+                ssl_context=self._ssl_context,
+                keepalive_expiry=self._keepalive_expiry,
+                http1=self._http1,
+                http2=self._http2,
+                network_backend=self._network_backend,
+            )
+
         return HTTPConnection(
             origin=origin,
             ssl_context=self._ssl_context,
diff --git a/httpcore/_sync/http_proxy.py b/httpcore/_sync/http_proxy.py
index e58693ecb..ecca88f7d 100644
--- a/httpcore/_sync/http_proxy.py
+++ b/httpcore/_sync/http_proxy.py
@@ -51,12 +51,7 @@ def merge_headers(
     return default_headers + override_headers
 
 
-def build_auth_header(username: bytes, password: bytes) -> bytes:
-    userpass = username + b":" + password
-    return b"Basic " + base64.b64encode(userpass)
-
-
-class HTTPProxy(ConnectionPool):
+class HTTPProxy(ConnectionPool):  # pragma: nocover
     """
     A connection pool that sends requests via an HTTP proxy.
     """
@@ -142,7 +137,8 @@ def __init__(
         if proxy_auth is not None:
             username = enforce_bytes(proxy_auth[0], name="proxy_auth")
             password = enforce_bytes(proxy_auth[1], name="proxy_auth")
-            authorization = build_auth_header(username, password)
+            userpass = username + b":" + password
+            authorization = b"Basic " + base64.b64encode(userpass)
             self._proxy_headers = [
                 (b"Proxy-Authorization", authorization)
             ] + self._proxy_headers
diff --git a/httpcore/_sync/socks_proxy.py b/httpcore/_sync/socks_proxy.py
index 61dd7e380..0ca96ddfb 100644
--- a/httpcore/_sync/socks_proxy.py
+++ b/httpcore/_sync/socks_proxy.py
@@ -102,7 +102,7 @@ def _init_socks5_connection(
         raise ProxyError(f"Proxy Server could not connect: {reply_code}.")
 
 
-class SOCKSProxy(ConnectionPool):
+class SOCKSProxy(ConnectionPool):  # pragma: nocover
     """
     A connection pool that sends requests via an HTTP proxy.
     """


Test patch:
diff --git a/tests/_async/test_http_proxy.py b/tests/_async/test_http_proxy.py
index b35fc2899..84a984b80 100644
--- a/tests/_async/test_http_proxy.py
+++ b/tests/_async/test_http_proxy.py
@@ -7,11 +7,12 @@
 
 from httpcore import (
     SOCKET_OPTION,
-    AsyncHTTPProxy,
+    AsyncConnectionPool,
     AsyncMockBackend,
     AsyncMockStream,
     AsyncNetworkStream,
     Origin,
+    Proxy,
     ProxyError,
 )
 
@@ -31,8 +32,8 @@ async def test_proxy_forwarding():
         ]
     )
 
-    async with AsyncHTTPProxy(
-        proxy_url="http://localhost:8080/",
+    async with AsyncConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         max_connections=10,
         network_backend=network_backend,
     ) as proxy:
@@ -87,8 +88,8 @@ async def test_proxy_tunneling():
         ]
     )
 
-    async with AsyncHTTPProxy(
-        proxy_url="http://localhost:8080/",
+    async with AsyncConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending an intial request, which once complete will return to the pool, IDLE.
@@ -178,8 +179,8 @@ async def test_proxy_tunneling_http2():
         ],
     )
 
-    async with AsyncHTTPProxy(
-        proxy_url="http://localhost:8080/",
+    async with AsyncConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         network_backend=network_backend,
         http2=True,
     ) as proxy:
@@ -227,8 +228,8 @@ async def test_proxy_tunneling_with_403():
         ]
     )
 
-    async with AsyncHTTPProxy(
-        proxy_url="http://localhost:8080/",
+    async with AsyncConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         with pytest.raises(ProxyError) as exc_info:
@@ -255,17 +256,23 @@ async def test_proxy_tunneling_with_auth():
         ]
     )
 
-    async with AsyncHTTPProxy(
-        proxy_url="http://localhost:8080/",
-        proxy_auth=("username", "password"),
+    async with AsyncConnectionPool(
+        proxy=Proxy(
+            url="http://localhost:8080/",
+            auth=("username", "password"),
+        ),
         network_backend=network_backend,
     ) as proxy:
         response = await proxy.request("GET", "https://example.com/")
         assert response.status == 200
         assert response.content == b"Hello, world!"
 
-        # Dig into this private property as a cheap lazy way of
-        # checking that the proxy header is set correctly.
-        assert proxy._proxy_headers == [  # type: ignore
-            (b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=")
-        ]
+
+def test_proxy_headers():
+    proxy = Proxy(
+        url="http://localhost:8080/",
+        auth=("username", "password"),
+    )
+    assert proxy.headers == [
+        (b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=")
+    ]
diff --git a/tests/_async/test_socks_proxy.py b/tests/_async/test_socks_proxy.py
index 3f5dd1cc0..907594a40 100644
--- a/tests/_async/test_socks_proxy.py
+++ b/tests/_async/test_socks_proxy.py
@@ -24,8 +24,8 @@ async def test_socks5_request():
         ]
     )
 
-    async with httpcore.AsyncSOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
+    async with httpcore.AsyncConnectionPool(
+        proxy=httpcore.Proxy("socks5://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending an intial request, which once complete will return to the pool, IDLE.
@@ -84,9 +84,11 @@ async def test_authenticated_socks5_request():
         ]
     )
 
-    async with httpcore.AsyncSOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
-        proxy_auth=(b"username", b"password"),
+    async with httpcore.AsyncConnectionPool(
+        proxy=httpcore.Proxy(
+            url="socks5://localhost:8080/",
+            auth=(b"username", b"password"),
+        ),
         network_backend=network_backend,
     ) as proxy:
         # Sending an intial request, which once complete will return to the pool, IDLE.
@@ -123,8 +125,8 @@ async def test_socks5_request_connect_failed():
         ]
     )
 
-    async with httpcore.AsyncSOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
+    async with httpcore.AsyncConnectionPool(
+        proxy=httpcore.Proxy("socks5://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending a request, which the proxy rejects
@@ -150,8 +152,8 @@ async def test_socks5_request_failed_to_provide_auth():
         ]
     )
 
-    async with httpcore.AsyncSOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
+    async with httpcore.AsyncConnectionPool(
+        proxy=httpcore.Proxy("socks5://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending a request, which the proxy rejects
@@ -180,9 +182,11 @@ async def test_socks5_request_incorrect_auth():
         ]
     )
 
-    async with httpcore.AsyncSOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
-        proxy_auth=(b"invalid", b"invalid"),
+    async with httpcore.AsyncConnectionPool(
+        proxy=httpcore.Proxy(
+            url="socks5://localhost:8080/",
+            auth=(b"invalid", b"invalid"),
+        ),
         network_backend=network_backend,
     ) as proxy:
         # Sending a request, which the proxy rejects
diff --git a/tests/_sync/test_http_proxy.py b/tests/_sync/test_http_proxy.py
index 2d66578e2..966672dd2 100644
--- a/tests/_sync/test_http_proxy.py
+++ b/tests/_sync/test_http_proxy.py
@@ -7,11 +7,12 @@
 
 from httpcore import (
     SOCKET_OPTION,
-    HTTPProxy,
+    ConnectionPool,
     MockBackend,
     MockStream,
     NetworkStream,
     Origin,
+    Proxy,
     ProxyError,
 )
 
@@ -31,8 +32,8 @@ def test_proxy_forwarding():
         ]
     )
 
-    with HTTPProxy(
-        proxy_url="http://localhost:8080/",
+    with ConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         max_connections=10,
         network_backend=network_backend,
     ) as proxy:
@@ -87,8 +88,8 @@ def test_proxy_tunneling():
         ]
     )
 
-    with HTTPProxy(
-        proxy_url="http://localhost:8080/",
+    with ConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending an intial request, which once complete will return to the pool, IDLE.
@@ -178,8 +179,8 @@ def test_proxy_tunneling_http2():
         ],
     )
 
-    with HTTPProxy(
-        proxy_url="http://localhost:8080/",
+    with ConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         network_backend=network_backend,
         http2=True,
     ) as proxy:
@@ -227,8 +228,8 @@ def test_proxy_tunneling_with_403():
         ]
     )
 
-    with HTTPProxy(
-        proxy_url="http://localhost:8080/",
+    with ConnectionPool(
+        proxy=Proxy("http://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         with pytest.raises(ProxyError) as exc_info:
@@ -255,17 +256,23 @@ def test_proxy_tunneling_with_auth():
         ]
     )
 
-    with HTTPProxy(
-        proxy_url="http://localhost:8080/",
-        proxy_auth=("username", "password"),
+    with ConnectionPool(
+        proxy=Proxy(
+            url="http://localhost:8080/",
+            auth=("username", "password"),
+        ),
         network_backend=network_backend,
     ) as proxy:
         response = proxy.request("GET", "https://example.com/")
         assert response.status == 200
         assert response.content == b"Hello, world!"
 
-        # Dig into this private property as a cheap lazy way of
-        # checking that the proxy header is set correctly.
-        assert proxy._proxy_headers == [  # type: ignore
-            (b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=")
-        ]
+
+def test_proxy_headers():
+    proxy = Proxy(
+        url="http://localhost:8080/",
+        auth=("username", "password"),
+    )
+    assert proxy.headers == [
+        (b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=")
+    ]
diff --git a/tests/_sync/test_socks_proxy.py b/tests/_sync/test_socks_proxy.py
index 2d39bb97a..89ec9faee 100644
--- a/tests/_sync/test_socks_proxy.py
+++ b/tests/_sync/test_socks_proxy.py
@@ -24,8 +24,8 @@ def test_socks5_request():
         ]
     )
 
-    with httpcore.SOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
+    with httpcore.ConnectionPool(
+        proxy=httpcore.Proxy("socks5://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending an intial request, which once complete will return to the pool, IDLE.
@@ -84,9 +84,11 @@ def test_authenticated_socks5_request():
         ]
     )
 
-    with httpcore.SOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
-        proxy_auth=(b"username", b"password"),
+    with httpcore.ConnectionPool(
+        proxy=httpcore.Proxy(
+            url="socks5://localhost:8080/",
+            auth=(b"username", b"password"),
+        ),
         network_backend=network_backend,
     ) as proxy:
         # Sending an intial request, which once complete will return to the pool, IDLE.
@@ -123,8 +125,8 @@ def test_socks5_request_connect_failed():
         ]
     )
 
-    with httpcore.SOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
+    with httpcore.ConnectionPool(
+        proxy=httpcore.Proxy("socks5://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending a request, which the proxy rejects
@@ -150,8 +152,8 @@ def test_socks5_request_failed_to_provide_auth():
         ]
     )
 
-    with httpcore.SOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
+    with httpcore.ConnectionPool(
+        proxy=httpcore.Proxy("socks5://localhost:8080/"),
         network_backend=network_backend,
     ) as proxy:
         # Sending a request, which the proxy rejects
@@ -180,9 +182,11 @@ def test_socks5_request_incorrect_auth():
         ]
     )
 
-    with httpcore.SOCKSProxy(
-        proxy_url="socks5://localhost:8080/",
-        proxy_auth=(b"invalid", b"invalid"),
+    with httpcore.ConnectionPool(
+        proxy=httpcore.Proxy(
+            url="socks5://localhost:8080/",
+            auth=(b"invalid", b"invalid"),
+        ),
         network_backend=network_backend,
     ) as proxy:
         # Sending a request, which the proxy rejects