Files
http-latency-prober/docs/usage/hxprobe.md
Jan Novak f487a4b1bd docs: hxprobe plans, summaries, explanations, usage, and changelog
Plans (docs/plans/):
- 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design
- 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage
  doc + Makefile, file input (-f), simplification pass, run-summary footer

Summaries (docs/summaries/): one per completed feature, recording what
was actually built, deviations from the plan, and verification steps

Explanations (docs/explanations/): two deep-dives written during review —
hxprobe concurrency model and worst-exit-code + render-loop analysis

Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md
for the full runnable reference

Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the
latprobe Python package for interview / code-review context

CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc,
file input, simplification, run-summary footer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:22:59 +02:00

233 lines
9.4 KiB
Markdown

# `hxprobe/` — httpx-based HTTP latency probe (Go-client parity)
> For a full case-by-case runnable reference (every flag, every failure
> mode, real captured output), see [`hxprobe/USAGE.md`](../../hxprobe/USAGE.md).
## What it does
A standalone Python CLI, independent of anything else in this repo (own
`pyproject.toml`, own venv — see [Setup](#setup)). It measures per-phase HTTP
latency like [`python/latprobe`](py-latprobe.md), but is built on the
[httpx](https://www.python-httpx.org/) library instead of raw sockets, so the
client matches Go's `http.DefaultClient`: **HTTP/2 negotiated via ALPN**,
**redirects followed by default**, connection pooling, and default TLS
verification — while still reporting the same six-phase breakdown (DNS, TCP
connect, TLS, TTFB, Transfer, Total).
Python has no equivalent of Go's `net/http/httptrace`, so the phase timing is
recovered by instrumenting httpx's network backend directly
(`hxprobe/probe.py`): a custom `NetworkBackend`/`NetworkStream` pair times
DNS, TCP connect, and TLS at the socket level, while HTTP framing (HTTP/1.1
or HTTP/2), redirect-following, and keep-alive stay entirely owned by httpx.
Run it from the `hxprobe/` directory with `uv run python -m hxprobe`, or via
`make hx-run ARGS="…"` from the repo root.
## Setup
`hxprobe` is a fully self-contained project — it could be copied out of this
repo into its own tomorrow and still work, with its own `pyproject.toml`,
lockfile (`uv.lock`), and [uv](https://docs.astral.sh/uv/)-managed venv
(`hxprobe/.venv`), separate from anything under `python/`. Install once:
```sh
make hx-deps
```
This runs `uv sync` inside `hxprobe/`, creating `.venv` and installing
`httpx` (plus `h2` for HTTP/2), `pytest`, and `ruff` from `uv.lock` — pinned,
reproducible versions, not whatever the resolver happens to pick at install
time. It's a prerequisite of `make hx-run`/`make hx-test`/etc., so those
targets set it up automatically on first run — `make hx-deps` is only needed
if you want to call `uv run python -m hxprobe` directly from inside
`hxprobe/`. See [hxprobe/README.md](../../hxprobe/README.md) for the
from-inside-the-directory quick start.
## Flags / arguments
Same surface as `latprobe`, plus two opt-outs for the Go-like defaults:
```
python -m hxprobe [flags] <url> [url ...]
```
| Flag | Default | Description |
|------|---------|-------------|
| `url …` (positional) | required | One or more URLs to probe |
| `-n N`, `--count N` | `1` | Number of requests per URL |
| `-c N`, `--concurrency N` | `0` (auto) | Max parallel URLs; `0` = `min(len(urls), 8)` |
| `--timeout DURATION` | `10s` | Per-request timeout; supports `ms`, `s`, `m`, or bare seconds |
| `--fail` | off | Exit non-zero when any HTTP status ≥ 400 |
| `--json` | off | Output as JSON array instead of text |
| `-v`, `--verbose` | off | Show resolved IP, negotiated protocol/redirects, TLS info, certificate, response headers |
| `--no-http2` | off (HTTP/2 on) | Disable HTTP/2 negotiation, force HTTP/1.1 |
| `--no-follow-redirects` | off (follow on) | Report the raw redirect response instead of following it |
| `-h`, `--help` | — | Show help and exit 0 |
**Exit codes:** identical to `latprobe` (0 ok, 1 usage, 2 dns, 3 connect,
4 timeout, 5 tls, 6 http≥400 with `--fail`); the highest code across all URLs
is returned. When more than one URL is probed, a summary footer is appended
after the last URL block tallying every URL's outcome (ok / dns / connect /
timeout / tls / http) and the resulting exit code — see "Multi-URL summary
footer" in [`hxprobe/USAGE.md`](../../hxprobe/USAGE.md) for a real example.
## Examples
### Single URL — negotiates HTTP/2 by default
```sh
make hx-run ARGS="https://example.com"
```
```
https://example.com (200)
DNS lookup : 2.81 ms
TCP connect : 8.28 ms
TLS handshake : 16.13 ms
Server (TTFB) : 1.04 ms
Transfer : 0.62 ms
─────────────────────────────
Total : 42.53 ms
```
### Verbose — shows the negotiated protocol
```sh
make hx-run ARGS="-v https://example.com"
```
```
https://example.com (200)
DNS lookup : 2.93 ms
TCP connect : 8.58 ms
TLS handshake : 17.71 ms
Server (TTFB) : 4.35 ms
Transfer : 0.84 ms
─────────────────────────────
Total : 44.06 ms
IP : 104.20.23.154
Protocol : HTTP/2
TLS : TLSv1.3 TLS_AES_256_GCM_SHA384 256 bit
Cert : CN=example.com valid until 2026-08-29 SSL Corporation
```
### Redirects followed by default
```sh
make hx-run ARGS="-v http://github.com"
```
```
http://github.com (200)
DNS lookup : 14.01 ms
TCP connect : 19.42 ms
TLS handshake : 22.72 ms
Server (TTFB) : 0.01 ms
Transfer : 65.66 ms
─────────────────────────────
Total : 196.72 ms
IP : 140.82.121.4
Protocol : HTTP/2 (1 redirect)
TLS : TLSv1.3 TLS_AES_128_GCM_SHA256 128 bit
Cert : CN=github.com valid until 2026-08-02 Sectigo Limited
```
Pass `--no-follow-redirects` to get the raw redirect response instead:
```sh
make hx-run ARGS="--no-follow-redirects http://github.com"
```
```
http://github.com (301)
DNS lookup : 2.85 ms
TCP connect : 24.16 ms
Server (TTFB) : 24.50 ms
Transfer : 0.59 ms
─────────────────────────────
Total : 52.58 ms
```
### Forcing HTTP/1.1
```sh
make hx-run ARGS="-v --no-http2 https://example.com"
```
```
https://example.com (200)
DNS lookup : 2.31 ms
TCP connect : 8.75 ms
TLS handshake : 13.61 ms
Server (TTFB) : 13.51 ms
Transfer : 0.44 ms
─────────────────────────────
Total : 39.07 ms
IP : 104.20.23.154
Protocol : HTTP/1.1
TLS : TLSv1.3 TLS_AES_256_GCM_SHA384 256 bit
Cert : CN=example.com valid until 2026-08-29 SSL Corporation
```
### Sampling, JSON, `--fail`, DNS failure
Same shape as `latprobe`'s own examples — see
[py-latprobe.md](py-latprobe.md#examples) for `-n`, `--json`, multi-URL,
`--fail`, and timeout output. hxprobe's JSON verbose object adds two keys:
```json
"verbose": {
"ip": "104.20.23.154",
"http_version": "HTTP/2",
"redirect_count": 1,
"tls_version": "TLSv1.3",
...
}
```
`http_version`/`redirect_count` are omitted from the text view when
`http_version` is unknown (only happens on failures before headers arrived).
## HTTP/2, redirects, and a TTFB accuracy note
**HTTP/2 and redirects** are the headline reasons this project exists
alongside the raw-socket `latprobe` — see [py-latprobe.md](py-latprobe.md) for
what that implementation does instead (always HTTP/1.1, never follows
redirects).
**A TTFB finding worth knowing, if you've compared numbers against another
raw-socket HTTP client:** during development, this implementation's TTFB
came in consistently ~40-50ms *lower* than a comparable raw-socket
implementation that didn't set `TCP_NODELAY` on its connect socket. That gap
wasn't noise — a socket that never sets `TCP_NODELAY` is subject to Nagle's
algorithm interacting with the server's delayed-ACK timer, a well-known
~40ms artifact. This implementation sets `TCP_NODELAY` on every connection
(matching both httpcore's own default backend and Go's `net.Dialer`),
avoiding that penalty. Forcing `TCP_NODELAY` onto the other socket
experimentally collapsed its TTFB to match this one's — confirming the
cause. If you're comparing hxprobe's numbers against some other HTTP/1.1
client that doesn't set `TCP_NODELAY`, expect this implementation's TTFB to
read lower, and correctly so.
**Redirect semantics for `dns`/`connect`/`tls` vs `ttfb`/`transfer`:** when
redirects are followed, `dns`/`connect`/`tls` (and the verbose IP/TLS/cert
fields) reflect the **first** connection only — "cost of reaching the origin
server." `ttfb`/`transfer` reflect the **last** hop, because each write/read
call overwrites them — which mirrors how Go's own `httptrace.ClientTrace`
hooks behave for a followed redirect (they aren't guarded either, so the last
hop wins there too).
## Limitations
- Always `GET`, no custom headers/body/auth — matches Go's `http.DefaultClient`.
- `Options.timeout` applies uniformly to connect/read/write/pool phases (a
single value); it is not split into separate per-phase budgets.
- No UNIX socket support.
- HTTP/2 requires TLS (`https://`) in practice — cleartext `h2c` is not
attempted for `http://` URLs (matches nearly every real HTTP/2 deployment).
## Makefile targets
```sh
make hx-deps # one-time: uv sync (venv + lockfile install)
make hx-run ARGS="-v -n 3 https://example.com" # run it
make hx-test # hermetic unit tests (pytest -m "not integration")
make hx-test-integration # live tests: real HTTP/2 negotiation, redirects
make hx-lint # ruff check
make hx-fmt # ruff format
make hx-check # lint + hermetic tests (the pre-commit-style gate)
```
`hx-check` (lint + hermetic tests) also runs as part of the repo-root
`make check`; `hx-test` runs as part of `make test`.