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>
7.1 KiB
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 thesocket.getaddrinfo→socket.connectsplit already inlatprobe/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", ...)(thestream()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; dependencyhttpx[http2](pullsh2). Makeslatprobe+hxprobepip install -e .-able; tests still run viaPYTHONPATH.python/hxprobe/__init__.pypython/hxprobe/probe.py—measure(url, opts) -> latprobe.probe.Result(imports & returns the sameResultso aggregation/rendering just work);_TimingBackend, the per-call trace, error classification, verbose capture.python/hxprobe/cli.py— thin: delegates tolatprobe.cli.run(...)passingmeasure_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, localhttp.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 viarun()withio.StringIO(mirrorstests/test_cli.py:73-76).python/tests/test_integration_hx.py— live, excluded from default gate (filename startstest_i…, so thetest_[!i]*.pyglob skips it): probe a real HTTP/2 host and assert negotiatedhttp_version == "h2"; guard with a@skipUnless(_online())liketests/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— parameterizerun()and_run_samples()with an injectablemeasure_fn(default = currentlatprobe.probe.measure), sohxprobereuses all argparse, concurrency, exit-code, text/JSON rendering logic. Extend_print_verbose_blockand_build_json_entryto showhttp_version/redirect_countwhen 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— addpy-deps(create.venv,pip install -e python),hx-run(cd python && python -m hxprobe $(ARGS)), and foldtest_hx_*into the existingpy-testgate; addhx-test-integrationfor 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
0–6 stay identical to Go/latprobe.
Verification
make py-deps— create venv, installhttpx[http2].make hx-run ARGS="https://example.com"— full 6-phase text output renders.make hx-run ARGS="-v https://www.cloudflare.com"— verbose block showshttp_version: h2and TLS/cert details.- Go-parity spot checks:
- HTTP/2:
python -m hxprobe -v <h2-host>reportsh2wherepython -m latprobe -v <h2-host>reports HTTP/1.1. - Redirects:
python -m hxprobe http://github.comfollows to https and showsredirect_count > 0(socketlatprobeshows a raw 301).
- HTTP/2:
make py-test— hermetic suite (now includingtest_hx_probe.py,test_hx_cli.py) is green;latprobe's existing tests still pass (proves themeasure_fn/Options/VerboseDetailedits are backward-compatible).make hx-test-integration— live test confirms realh2negotiation.- Confirm
--jsonoutput forhxprobematches thelatprobeschema plus the optionalverbose.http_version/verbose.redirect_countkeys.