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>
5.6 KiB
Summary: hxprobe — httpx-based Python probe
Plan: docs/plans/2026-07-01-23-47-py-hxprobe-httpx.md
What was built
A new sibling package, python/hxprobe/, alongside the existing raw-socket
latprobe. It uses httpx 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 full
six-phase breakdown (DNS, TCP connect, TLS, TTFB, Transfer, Total).
Files:
python/pyproject.toml— first third-party dependency in this repo (httpx[http2])python/hxprobe/probe.py— the core:_Trace,_TimingStream,_TimingBackend,_TimingTransport,measure()python/hxprobe/cli.py,__main__.py,__init__.py— thin wrapperspython/latprobe/probe.py(edited) — addedOptions.follow_redirects/Options.http2(ignored by the socketmeasure()) andVerboseDetail.http_version/redirect_count(always""/0there)python/latprobe/cli.py(edited) —run()/_run_samples()take an injectablemeasure_fn, plusprog/description/protocol_flagsoverrides, sohxprobe.cli.run()reuses the entire argparse/concurrency/ exit-code/rendering pipeline unchangedpython/tests/test_hx_probe.py(17 tests),test_hx_cli.py(11 tests),test_integration_hx.py(9 live tests, excluded from the default gate via the existingtest_i*naming convention)Makefile—py-deps(createspython/.venv, installshttpx[http2]),hx-run,hx-test-integration;py-test/py-checknow run through the venv and include the new hermetic hxprobe testsdocs/usage/py-hxprobe.md— usage doc with real captured output.gitignore— added*.egg-info/(editable-install artifact)
Key design decisions
- Instrument the transport, don't hand-roll HTTP. Subclassed
httpcore.NetworkBackend/NetworkStreamto time DNS/TCP connect/TLS at the socket level, letting httpx own HTTP/1.1 vs HTTP/2 framing, redirects, and keep-alive. This was the reason to use httpx at all — get the protocol behavior of a real client while keeping latprobe's phase granularity. - First-hop-wins for dns/connect/tls; last-hop-wins for ttfb/transfer.
When redirects are followed, connection-identity fields (dns/connect/tls
timing, resolved IP, TLS/cert info) reflect the first connection.
wrote_request/first_byteare simply overwritten on every write/read, so they naturally end up reflecting the last hop — which mirrors how Go's own unguardedhttptrace.ClientTracehooks behave for a followed redirect. - Fresh
httpx.Clientpermeasure()call, no cross-sample pooling — matches latprobe's per-call socket creation so every-nsample gets a full phase breakdown. measure_fninjection over subclassing/duplication inlatprobe.cli, so hxprobe reuses argparse, concurrency, exit codes, and text/JSON rendering with zero duplicated logic — the socket and httpx probes only differ inprobe.py.- New CLI flags gated behind
protocol_flags=Truesolatprobe's own--helpoutput stays byte-for-byte unchanged (verified) —--no-http2/--no-follow-redirectsonly appear forhxprobe.
Notable finding (not part of the original plan)
While comparing hxprobe and latprobe timings against the same live host,
hxprobe's TTFB was consistently ~40-50ms lower. Verified experimentally
(not just assumed) that this is a real effect, not noise: latprobe's raw
socket never sets TCP_NODELAY, so its request write is subject to Nagle's
algorithm interacting with the server's delayed-ACK timer — a well-known
artifact. Forcing TCP_NODELAY onto latprobe's socket collapsed its TTFB
to match hxprobe's. hxprobe sets TCP_NODELAY (matching httpcore's own
default backend and Go's net.Dialer), so its TTFB numbers are the more
accurate of the two — not just different. Did not change latprobe itself
(out of scope for this task); documented the divergence in
docs/usage/py-hxprobe.md, in the CHANGELOG, and inline in
hxprobe/probe.py.
Deviations from the plan
- Plan sketched
TTFB = headers-received minus end-of-TLS; implemented asTTFB = first-byte minus wrote-requestinstead (matches both Go and the existinglatprobedefinition — the plan's phrasing was an approximation). - Plan said verbose TLS/cert metadata could follow "last hop"; implemented
as first-hop-wins uniformly across dns/connect/tls/ip/tls-info for
simplicity and consistency (only
ttfb/transferare last-hop). - Everything else (library choice, transport-instrumentation approach, pyproject.toml, sibling-package placement, flag surface) matches the approved plan as written.
Verification
make check(Go + Python full gate): exit 0, 83 hermetic Python tests (72 pre-existing + 11 new hermetic CLI + hxprobe's share of the 17 probe tests already counted), zero regressions to latprobe's original 39.make hx-test-integration: 9/9 live tests pass, including real HTTP/2 negotiation against example.com (Cloudflare) and a real http→https redirect follow against github.com.- Manual spot checks:
--jsonschema,--failexit code 6, DNS failure (exit 2), connection-refused (exit 3),--no-http2/--no-follow-redirectsflag behavior,latprobe --helpoutput diffed byte-for-byte against pre-change output to confirm no regression. - Caught and reverted an incidental
gofmtwhitespace diff ingo/internal/probe/probe.gothatmake check'sgo-fmtstep produced — unrelated to this change, out of scope, not committed.