Files
http-latency-prober/docs/plans/2026-07-01-23-47-py-hxprobe-httpx.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

131 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: `hxprobe` — httpx-based Python probe (Go-client parity)
## Context
The existing Python `latprobe` package measures per-phase HTTP latency with raw
sockets. That gives an excellent DNS/TCP/TLS/TTFB/Transfer breakdown, but the
cost is that it always speaks **HTTP/1.1** and **does not follow redirects**
diverging from the Go implementation, whose `http.DefaultClient` negotiates
**HTTP/2** via ALPN, **follows redirects** (up to 10), pools connections, and
verifies TLS by default (confirmed: `go/internal/probe/probe.go` uses
`http.DefaultClient.Do` with no custom transport/`CheckRedirect`).
Goal: add a **second Python implementation, `hxprobe`**, built on the `httpx`
library so it matches the Go client's protocol capabilities (HTTP/2, redirects,
pooling, TLS verification) **while preserving the full 6-phase timing** that is
latprobe's whole point. Python has no equivalent of Go's `net/http/httptrace`,
so the phase breakdown is recovered by instrumenting httpx's network backend.
This is an **additive, Python-only experiment** — intentionally outside the
CLAUDE.md "Go first, then Python port" flow, since the user explicitly asked for
a library-based Python variant. No Go change is required.
## Approach
New sibling package `python/hxprobe/`, reusing everything reusable from
`latprobe` (dataclasses, aggregation, duration parsing, CLI rendering) so the
only genuinely new code is the httpx probe backend.
### Key design: instrumented httpx transport
`httpx` (sync `httpx.Client`) runs on `httpcore`. To recover per-phase timing we
subclass httpcore's sync network backend and time the phases at the socket
level, letting httpx own HTTP framing, HTTP/2, redirects, and keep-alive:
- `connect_tcp(...)` — reimplement DNS + TCP as separate steps (port the
`socket.getaddrinfo``socket.connect` split already in
`latprobe/probe.py:167-203`), timestamping **DNS** and **TCP connect**
independently, and capturing the resolved IP.
- `start_tls(...)` — timestamp the **TLS handshake**; pull negotiated version /
cipher / peer cert from the SSL object for verbose mode.
- **TTFB** = headers-received minus end-of-TLS (server processing), measured via
`client.stream("GET", ...)` (the `stream()` context yields once response
headers arrive).
- **Transfer** = iterating `resp.iter_raw()` to EOF, minus headers-received.
- **Total** = wraps the whole `measure()` call.
Each `measure()` call uses a **fresh `httpx.Client` (no cross-sample pooling)** so
every `-n` sample yields a full phase breakdown — matching the current
raw-socket `latprobe` behavior rather than Go's pool-reuse quirk.
A per-call trace object (held by the backend instance) records timings and the
`fail_phase` at the exact point a phase raises, giving precise error
classification (`dns`/`connect`/`timeout`/`tls`/`transfer`/`request`) without
guessing from httpx exception types.
**Redirects (followed by default, Go parity):** DNS/connect/TLS are reported
from the **first** connection (mirrors Go's `connectStart.IsZero()` guard);
TTFB/Transfer/Total span the full followed chain. `redirect_count` and the
negotiated `http_version` (`h2` vs `http/1.1`) are surfaced as new verbose
fields — a genuine capability the socket version lacks.
## Files
**New:**
- `python/pyproject.toml` — project metadata; dependency `httpx[http2]` (pulls
`h2`). Makes `latprobe` + `hxprobe` `pip install -e .`-able; tests still run
via `PYTHONPATH`.
- `python/hxprobe/__init__.py`
- `python/hxprobe/probe.py``measure(url, opts) -> latprobe.probe.Result`
(imports & returns the **same `Result`** so aggregation/rendering just work);
`_TimingBackend`, the per-call trace, error classification, verbose capture.
- `python/hxprobe/cli.py` — thin: delegates to `latprobe.cli.run(...)` passing
`measure_fn=hxprobe.probe.measure` (see reuse edit below).
- `python/hxprobe/__main__.py``sys.exit(cli.run(sys.argv[1:], ...))`.
- `python/tests/test_hx_probe.py` — hermetic, local `http.server`: phase
presence, **redirect following** (302 handler — validates the Go-parity
feature), connection-refused → `connect`, black-hole port → `timeout`.
- `python/tests/test_hx_cli.py` — hermetic CLI via `run()` with `io.StringIO`
(mirrors `tests/test_cli.py:73-76`).
- `python/tests/test_integration_hx.py` — live, **excluded from default gate**
(filename starts `test_i…`, so the `test_[!i]*.py` glob skips it): probe a
real HTTP/2 host and assert negotiated `http_version == "h2"`; guard with a
`@skipUnless(_online())` like `tests/test_integration.py:29-40`.
- `docs/usage/py-hxprobe.md` — usage doc (what it does, flags, example with
expected output, and an explicit socket-vs-httpx capability comparison table),
per CLAUDE.md.
**Edited (small, backward-compatible):**
- `python/latprobe/cli.py` — parameterize `run()` and `_run_samples()` with an
injectable `measure_fn` (default = current `latprobe.probe.measure`), so
`hxprobe` reuses all argparse, concurrency, exit-code, text/JSON rendering
logic. Extend `_print_verbose_block` and `_build_json_entry` to show
`http_version` / `redirect_count` **when present** (existing socket path never
sets them → output unchanged).
- `python/latprobe/probe.py` — add optional fields with safe defaults:
`Options.follow_redirects=True`, `Options.http2=True` (ignored by the socket
measure); `VerboseDetail.http_version=""`, `VerboseDetail.redirect_count=0`.
- `Makefile` — add `py-deps` (create `.venv`, `pip install -e python`),
`hx-run` (`cd python && python -m hxprobe $(ARGS)`), and fold `test_hx_*` into
the existing `py-test` gate; add `hx-test-integration` for the live h2 check.
- `CHANGELOG.md` — append a timestamped one-line entry on completion.
**Reused as-is:** `latprobe.aggregate.summarize`,
`latprobe.duration.parse_duration`, `latprobe.probe.{Result,Phase,CertInfo}`,
and the entire `latprobe.cli` renderer via the `measure_fn` injection.
## Flags / behavior
Same surface as `latprobe` (`-n/--count`, `-c/--concurrency`, `--timeout`,
`--fail`, `--json`, `-v/--verbose`) via the reused parser, plus two new opt-outs
for the Go-like defaults: `--no-http2` and `--no-follow-redirects`. Exit codes
06 stay identical to Go/`latprobe`.
## Verification
1. `make py-deps` — create venv, install `httpx[http2]`.
2. `make hx-run ARGS="https://example.com"` — full 6-phase text output renders.
3. `make hx-run ARGS="-v https://www.cloudflare.com"` — verbose block shows
`http_version: h2` and TLS/cert details.
4. **Go-parity spot checks:**
- HTTP/2: `python -m hxprobe -v <h2-host>` reports `h2` where
`python -m latprobe -v <h2-host>` reports HTTP/1.1.
- Redirects: `python -m hxprobe http://github.com` follows to https and shows
`redirect_count > 0` (socket `latprobe` shows a raw 301).
5. `make py-test` — hermetic suite (now including `test_hx_probe.py`,
`test_hx_cli.py`) is green; `latprobe`'s existing tests still pass (proves the
`measure_fn`/`Options`/`VerboseDetail` edits are backward-compatible).
6. `make hx-test-integration` — live test confirms real `h2` negotiation.
7. Confirm `--json` output for `hxprobe` matches the `latprobe` schema plus the
optional `verbose.http_version` / `verbose.redirect_count` keys.