# Network Backends
The lowest level network abstractions in `httpx` are the `NetworkBackend` and `NetworkStream` classes. These provide a consistent interface onto the operations for working with a network stream, typically over a TCP connection. Different runtimes (threaded, trio & asyncio) are supported via alternative implementations of the core interface.
---
## `NetworkBackend()`
The default backend is instantiated via the `NetworkBackend` class...
```{ .python .httpx }
>>> net = httpx.NetworkBackend()
>>> net
```
```{ .python .ahttpx .hidden }
>>> net = ahttpx.NetworkBackend()
>>> net
```
### `.connect(host, port)`
A TCP stream is created using the `connect` method...
```{ .python .httpx }
>>> net = httpx.NetworkBackend()
>>> stream = net.connect("www.encode.io", 80)
>>> stream
```
```{ .python .ahttpx .hidden }
>>> net = ahttpx.NetworkBackend()
>>> stream = await net.connect("www.encode.io", 80)
>>> stream
```
Streams support being used in a context managed style. The cleanest approach to resource management is to use `.connect(...)` in the context of a `with` block.
```{ .python .httpx }
>>> net = httpx.NetworkBackend()
>>> with net.connect("dev.encode.io", 80) as stream:
>>> ...
>>> stream
```
```{ .python .ahttpx .hidden }
>>> net = ahttpx.NetworkBackend()
>>> async with await net.connect("dev.encode.io", 80) as stream:
>>> ...
>>> stream
```
## `NetworkStream(sock)`
The `NetworkStream` class provides TCP stream abstraction, by providing a thin wrapper around a socket instance.
Network streams do not provide any built-in thread or task locking.
Within `httpx` thread and task saftey is handled at the `Connection` layer.
### `.read(max_bytes=None)`
Read up to `max_bytes` bytes of data from the network stream.
If no limit is provided a default value of 64KB will be used.
### `.write(data)`
Write the given bytes of `data` to the network stream.
### `.start_tls(ctx, hostname)`
Upgrade a stream to TLS (SSL) connection for sending secure `https://` requests.
``
### `.get_extra_info(key)`
Return information about the underlying resource. May include...
* `"client_addr"` - Return the client IP and port.
* `"server_addr"` - Return the server IP and port.
* `"ssl_object"` - Return an `ssl.SSLObject` instance.
* `"socket"` - Access the raw socket instance.
### `.close()`
Close the network stream. For TLS streams this will attempt to send a closing handshake before terminating the conmection.
```{ .python .httpx }
>>> net = httpx.NetworkBackend()
>>> stream = net.connect("dev.encode.io", 80)
>>> try:
>>> ...
>>> finally:
>>> stream.close()
>>> stream
```
```{ .python .ahttpx .hidden }
>>> net = ahttpx.NetworkBackend()
>>> stream = await net.connect("dev.encode.io", 80)
>>> try:
>>> ...
>>> finally:
>>> await stream.close()
>>> stream
```
---
## Timeouts
Network timeouts are handled using a context block API.
This [design approach](https://vorpus.org/blog/timeouts-and-cancellation-for-humans) avoids timeouts needing to passed around throughout the stack, and provides an obvious and natural API to dealing with timeout contexts.
### timeout(duration)
The timeout context manager can be used to wrap socket operations anywhere in the stack.
Here's an example of enforcing an overall 3 second timeout on a request.
```{ .python .httpx }
>>> with httpx.Client() as cli:
>>> with httpx.timeout(3.0):
>>> res = cli.get('https://www.example.com')
>>> print(res)
```
```{ .python .ahttpx .hidden }
>>> async with ahttpx.Client() as cli:
>>> async with ahttpx.timeout(3.0):
>>> res = await cli.get('https://www.example.com')
>>> print(res)
```
Timeout contexts provide an API allowing for deadlines to be cancelled.
### .cancel()
In this example we enforce a 3 second timeout on *receiving the start of* a streaming HTTP response...
```{ .python .httpx }
>>> with httpx.Client() as cli:
>>> with httpx.timeout(3.0) as t:
>>> with cli.stream('https://www.example.com') as r:
>>> t.cancel()
>>> print(">>>", res)
>>> for chunk in r.stream:
>>> print("...", chunk)
```
```{ .python .ahttpx .hidden }
>>> async with ahttpx.Client() as cli:
>>> async with ahttpx.timeout(3.0) as t:
>>> async with await cli.stream('https://www.example.com') as r:
>>> t.cancel()
>>> print(">>>", res)
>>> async for chunk in r.stream:
>>> print("...", chunk)
```
---
## Sending HTTP requests
Let's take a look at how we can work directly with a network backend to send an HTTP request, and recieve an HTTP response.
```{ .python .httpx }
import httpx
import ssl
import truststore
net = httpx.NetworkBackend()
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
req = b'\r\n'.join([
b'GET / HTTP/1.1',
b'Host: www.example.com',
b'User-Agent: python/dev',
b'Connection: close',
b'',
b'',
])
# Use a 10 second overall timeout for the entire request/response.
with httpx.timeout(10.0):
# Use a 3 second timeout for the initial connection.
with httpx.timeout(3.0) as t:
# Open the connection & establish SSL.
with net.connect("www.example.com", 443) as stream:
stream.start_tls(ctx, hostname="www.example.com")
t.cancel()
# Send the request & read the response.
stream.write(req)
buffer = []
while part := stream.read():
buffer.append(part)
resp = b''.join(buffer)
```
```{ .python .ahttpx .hidden }
import ahttpx
import ssl
import truststore
net = ahttpx.NetworkBackend()
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
req = b'\r\n'.join([
b'GET / HTTP/1.1',
b'Host: www.example.com',
b'User-Agent: python/dev',
b'Connection: close',
b'',
b'',
])
# Use a 10 second overall timeout for the entire request/response.
async with ahttpx.timeout(10.0):
# Use a 3 second timeout for the initial connection.
async with ahttpx.timeout(3.0) as t:
# Open the connection & establish SSL.
async with await net.connect("www.example.com", 443) as stream:
await stream.start_tls(ctx, hostname="www.example.com")
t.cancel()
# Send the request & read the response.
await stream.write(req)
buffer = []
while part := await stream.read():
buffer.append(part)
resp = b''.join(buffer)
```
The example above is somewhat contrived, there's no HTTP parsing implemented so we can't actually determine when the response is complete. We're using a `Connection: close` header to request that the server close the connection once the response is complete.
A more complete example would require proper HTTP parsing. The `Connection` class implements an HTTP request/response interface, layered over a `NetworkStream`.
---
## Custom network backends
The interface for implementing custom network backends is provided by two classes...
### `NetworkBackendInterface`
The abstract interface implemented by `NetworkBackend`. See above for details.
### `NetworkStreamInterface`
The abstract interface implemented by `NetworkStream`. See above for details.
### An example backend
We can use these interfaces to implement custom functionality. For example, here we're providing a network backend that logs all the ingoing and outgoing bytes.
```{ .python .httpx }
class RecordingBackend(httpx.NetworkBackendInterface):
def __init__(self):
self._backend = NetworkBackend()
def connect(self, host, port):
# Delegate creating connections to the default
# network backend, and return a wrapped stream.
stream = self._backend.connect(host, port)
return RecordingStream(stream)
class RecordingStream(httpx.NetworkStreamInterface):
def __init__(self, stream):
self._stream = stream
def read(self, max_bytes: int = None):
# Print all incoming data to the terminal.
data = self._stream.read(max_bytes)
lines = data.decode('ascii', errors='replace').splitlines()
for line in lines:
print("<<< ", line)
return data
def write(self, data):
# Print all outgoing data to the terminal.
lines = data.decode('ascii', errors='replace').splitlines()
for line in lines:
print(">>> ", line)
self._stream.write(data)
def start_tls(ctx, hostname):
self._stream.start_tls(ctx, hostname)
def get_extra_info(key):
return self._stream.get_extra_info(key)
def close():
self._stream.close()
```
```{ .python .ahttpx .hidden }
class RecordingBackend(ahhtpx.NetworkBackendInterface):
def __init__(self):
self._backend = NetworkBackend()
async def connect(self, host, port):
# Delegate creating connections to the default
# network backend, and return a wrapped stream.
stream = await self._backend.connect(host, port)
return RecordingStream(stream)
class RecordingStream(ahttpx.NetworkStreamInterface):
def __init__(self, stream):
self._stream = stream
async def read(self, max_bytes: int = None):
# Print all incoming data to the terminal.
data = await self._stream.read(max_bytes)
lines = data.decode('ascii', errors='replace').splitlines()
for line in lines:
print("<<< ", line)
return data
async def write(self, data):
# Print all outgoing data to the terminal.
lines = data.decode('ascii', errors='replace').splitlines()
for line in lines:
print(">>> ", line)
await self._stream.write(data)
async def start_tls(ctx, hostname):
await self._stream.start_tls(ctx, hostname)
def get_extra_info(key):
return self._stream.get_extra_info(key)
async def close():
await self._stream.close()
```
We can now instantiate a client using this network backend.
```{ .python .httpx }
>>> transport = httpx.ConnectionPool(backend=RecordingBackend())
>>> cli = httpx.Client(transport=transport)
>>> cli.get('https://www.example.com')
```
```{ .python .ahttpx .hidden }
>>> transport = ahttpx.ConnectionPool(backend=RecordingBackend())
>>> cli = ahttpx.Client(transport=transport)
>>> await cli.get('https://www.example.com')
```
Custom network backends can also be used to provide functionality such as handling DNS caching for name lookups, or connecting via a UNIX domain socket instead of a TCP connection.
---
← [Parsers](parsers.md)