# HTTPX async proxy and pool-timeout lab

This download reproduces a client-side `PoolTimeout` using a real HTTP/1.1
forward proxy and origin on `127.0.0.1`. Both are temporary Python fixtures.
It also includes a small client example you can adapt to an authorized target.
No proxy account or credentials are needed for the lab.

Requires Python 3.11 or newer because the example uses `asyncio.timeout`.
The recorded run used CPython 3.14.7 on macOS arm64, HTTPX 0.28.1 and
HTTPcore 1.0.9. `requirements.txt` pins the full tested dependency set.
HTTPX 0.28.1 was the stable version reported by PyPI on 13 September 2026;
that is a dated observation, not a promise about future releases.

## Run the local lab

After downloading `httpx-async-proxy.zip`, run these commands in a terminal
on macOS or Linux. Package installation uses the internet; the demo and
tests use loopback sockets only.

```sh
unzip httpx-async-proxy.zip
cd httpx-async-proxy
python3 -m venv .venv
.venv/bin/python -m pip install --requirement requirements.txt
.venv/bin/python pool_timeout_demo.py
.venv/bin/python -W error::ResourceWarning -m unittest -v test_httpx_proxy.py
```

The demo exits with status 0 only after verifying all of these conditions:

1. Two manually streamed responses have received headers but still hold
   incomplete bodies inside a two-connection pool.
2. The third attempt, `/blocked`, raises `httpx.PoolTimeout`. Neither the
   proxy nor origin receives that request.
3. `await held[0].aclose()` releases capacity. A fresh `/ok` request through
   the same client returns the complete expected JSON body while the second
   held stream remains open.
4. All held responses, the client, fixture handlers, relay tasks, writers
   and listeners close. Normal completion requires no forced handler
   cancellation and leaves no new asyncio tasks.

The JSON includes ordered `client`, `proxy` and `origin` events, separate
request lists, the actual package/runtime versions, a UTC timestamp and
cleanup checks. Both server lists should be:

```json
["/hold/1", "/hold/2", "/ok"]
```

`pool_timeout_seconds` is configured to `0.25`; observed elapsed time also
includes local scheduling overhead. It is not a proxy latency measurement.
The fixture coordinates readiness and peer closure with events and socket
EOF, without timed sleeps. Its own timeouts bound a broken test.

## Adapt the small request example

`async_proxy_check.py` issues exactly three GET requests to one target
through one scoped `AsyncClient`. A semaphore admits at most two requests
at once. Each 10-second operation deadline includes admission waiting,
pool waiting and body reading. The client separately sets connect, read,
write and pool timeouts. These small values demonstrate the mechanism;
choose production limits and budgets for your own workload.

Set `PROXY_URL` and `TARGET_URL` in your environment to an authorized proxy
and an authorized small response endpoint, then run:

```sh
.venv/bin/python async_proxy_check.py
```

No real proxy or external target was used in the recorded tests. The test
suite runs this exact CLI against the controlled loopback fixture and
checks the full known body using its size and SHA-256.

The script prints status, decoded body byte count and SHA-256 for a fully
read response, or a bounded error class name. It exits nonzero if any
request fails. `HTTPStatusError` means a non-success HTTP response;
`BodyTooLarge` means more than 65,536 decoded bytes were encountered;
`OperationDeadline` means the outer 10-second deadline expired. It does
not log request/proxy URLs, credentials, raw response bodies or exception
messages. The decoded byte limit bounds what this example accepts, not
all internal decoder memory. Use a suitable content validator for your
target: HTTP 200, matching size, or matching hash alone does not establish
that an unfamiliar response contains the data you needed.

The `async with client.stream(...)` block closes its response on success,
HTTP errors, body-limit failures and cancellation. If you instead use
`client.send(..., stream=True)`, retain a `finally` block that calls
`await response.aclose()`; the deliberately held streams in the demo show
why this matters. Do not copy their delayed closure into a normal worker.

## Scope and boundaries

The fixture handles only HTTP/1.1 GET forwarding to its own ephemeral
loopback origin. It deliberately sends `Connection: close`, so it proves
reuse of the client and release of pool capacity, not reuse of a particular
TCP connection. An unfinished stream may have to discard its connection
when closed. `max_keepalive_connections` limits idle connections; it does
not free the two active streams in this demonstration.

`make_client()` keeps TLS verification enabled and disables redirects and
HTTP/2. `trust_env=False` makes configuration explicit: environment proxy
rules and environment CA settings such as `SSL_CERT_FILE` and
`SSL_CERT_DIR` are ignored. If your deployment needs a private CA, pass a
reviewed `ssl.SSLContext` through HTTPX's `verify=` option; that is outside
this loopback test.

HTTPS destinations normally use CONNECT through an HTTP proxy. The proxy
URL scheme describes the connection to the proxy; an HTTPS destination
does not by itself require an `https://` proxy URL. This fixture does not
implement or verify CONNECT, TLS handshakes, HTTPS proxy transport, proxy
authentication, HTTP/2, SOCKS, redirects, retries, remote DNS or external
service behavior. No provider speed, reliability or availability result
can be inferred from this synthetic test.

The six regression tests exercise pool exhaustion/recovery, the exact CLI
under conflicting environment settings, HTTP error cleanup, oversized body
cleanup, cancellation cleanup and fixture refusal of external destinations.

Primary references: [HTTPX async/stream lifecycle](https://www.python-httpx.org/async/),
[proxy configuration](https://www.python-httpx.org/advanced/proxies/),
[resource limits](https://www.python-httpx.org/advanced/resource-limits/),
[timeout phases](https://www.python-httpx.org/advanced/timeouts/),
[environment variables](https://www.python-httpx.org/environment_variables/),
and [SSL configuration](https://www.python-httpx.org/advanced/ssl/).
