Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/advanced/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ connecting via a Unix Domain Socket that is only available via this low-level AP
{"ID": "...", "Containers": 4, "Images": 74, ...}
```

Another advanced configuration is supplying a custom httpcore [Network Backend](https://www.encode.io/httpcore/network-backends/).

```pycon
>>> import httpx
>>> import myk8s
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

@iciclespider iciclespider Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a theoretical package that returns a theoretical NetwokBackend. Any suggestion for an alternative would be welcome.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the network backend examples to use httpcore.MockBackend.

>>> # This custom network backend enables remote access to ports inside a Kubernetes Cluster using pod port forwarding.
>>> backend = myk8s.NetworkBackend('cluster.local')
>>> transport = httpx.HTTPTransport(network_backend=backend)
>>> client = httpx.Client(transport=transport)
>>> response = client.get("http://argocd-server.argocd.svc.cluster.local")
```

## WSGI Transport

You can configure an `httpx` client to call directly into a Python web application using the WSGI protocol.
Expand Down
18 changes: 18 additions & 0 deletions httpx/_transports/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* uds: str
* local_address: str
* retries: int
* network_backend: httpcore.NetworkBackend

Example usages...

Expand All @@ -22,6 +23,13 @@
# Using advanced httpcore configuration, with unix domain sockets.
transport = httpx.HTTPTransport(uds="socket.uds")
client = httpx.Client(transport=transport)

# Using advanced httpcore configuration, with custom network backend.
import myk8s
backend = myk8s.NetworkBackend('cluster.local')
transport = httpx.HTTPTransport(network_backend=backend)
client = httpx.Client(transport=transport)
response = client.get("http://argocd-server.argocd.svc.cluster.local")
"""

from __future__ import annotations
Expand All @@ -33,6 +41,8 @@
if typing.TYPE_CHECKING:
import ssl # pragma: no cover

import httpcore # pragma: no cover

import httpx # pragma: no cover

from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
Expand Down Expand Up @@ -146,6 +156,7 @@ def __init__(
local_address: str | None = None,
retries: int = 0,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
network_backend: httpcore.NetworkBackend | None = None,
) -> None:
import httpcore

Expand All @@ -164,6 +175,7 @@ def __init__(
local_address=local_address,
retries=retries,
socket_options=socket_options,
network_backend=network_backend,
)
elif proxy.url.scheme in ("http", "https"):
self._pool = httpcore.HTTPProxy(
Expand All @@ -183,6 +195,7 @@ def __init__(
http1=http1,
http2=http2,
socket_options=socket_options,
network_backend=network_backend,
)
elif proxy.url.scheme in ("socks5", "socks5h"):
try:
Expand All @@ -207,6 +220,7 @@ def __init__(
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
network_backend=network_backend,
)
else: # pragma: no cover
raise ValueError(
Expand Down Expand Up @@ -290,6 +304,7 @@ def __init__(
local_address: str | None = None,
retries: int = 0,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
network_backend: httpcore.AsyncNetworkBackend | None = None,
) -> None:
import httpcore

Expand All @@ -308,6 +323,7 @@ def __init__(
local_address=local_address,
retries=retries,
socket_options=socket_options,
network_backend=network_backend,
)
elif proxy.url.scheme in ("http", "https"):
self._pool = httpcore.AsyncHTTPProxy(
Expand All @@ -327,6 +343,7 @@ def __init__(
http1=http1,
http2=http2,
socket_options=socket_options,
network_backend=network_backend,
)
elif proxy.url.scheme in ("socks5", "socks5h"):
try:
Expand All @@ -351,6 +368,7 @@ def __init__(
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
network_backend=network_backend,
)
else: # pragma: no cover
raise ValueError(
Expand Down
97 changes: 97 additions & 0 deletions tests/client/test_network_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import typing

import httpcore
import pytest

import httpx


def test_network_backend():
class Backend(httpcore.NetworkBackend):
def connect_tcp(
self,
host: str,
port: int,
timeout: typing.Optional[float] = None,
local_address: typing.Optional[str] = None,
socket_options: typing.Optional[
typing.Iterable[httpcore.SOCKET_OPTION]
] = None,
) -> httpcore.NetworkStream:
return Stream()

class Stream(httpcore.NetworkStream):
body = b"\r\n".join(
[
b"HTTP/1.1 200 OK",
b"",
b"From Backend!",
]
)

def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes:
body = self.body
if body:
self.body = b""
return body

def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None:
pass

def close(self) -> None:
pass

backend = Backend()
transport = httpx.HTTPTransport(network_backend=backend)
with httpx.Client(transport=transport) as client:
response = client.get("http://www.example.org")
assert response.status_code == 200
assert response.text == "From Backend!"


@pytest.mark.anyio
async def test_async_network_backend():
class AsyncBackend(httpcore.AsyncNetworkBackend):
async def connect_tcp(
self,
host: str,
port: int,
timeout: typing.Optional[float] = None,
local_address: typing.Optional[str] = None,
socket_options: typing.Optional[
typing.Iterable[httpcore.SOCKET_OPTION]
] = None,
) -> httpcore.AsyncNetworkStream:
return AsyncStream()

class AsyncStream(httpcore.AsyncNetworkStream):
body = b"\r\n".join(
[
b"HTTP/1.1 200 OK",
b"",
b"From Async Backend!",
]
)

async def read(
self, max_bytes: int, timeout: typing.Optional[float] = None
) -> bytes:
body = self.body
if body:
self.body = b""
return body

async def write(
self, buffer: bytes, timeout: typing.Optional[float] = None
) -> None:
pass

async def aclose(self) -> None:
pass

backend = AsyncBackend()
transport = httpx.AsyncHTTPTransport(network_backend=backend)
async with httpx.AsyncClient(transport=transport) as client:
response = await client.get("http://www.example.org")
assert response.status_code == 200
assert response.text == "From Async Backend!"