Files
http-latency-prober/docs/plans/2026-07-01-12-04-py-phases.md
Jan Novak f609d8124f feat(py): step 2 — per-phase latency measurement (phases.py)
Hand-drives raw sockets to time each HTTP phase individually — DNS
(getaddrinfo), TCP connect, TLS handshake (ssl.wrap_socket, https only),
TTFB (sendall → first recv), Transfer (first byte → EOF), Total.

Partial phases are preserved on failure (same invariant as Go's probe.go).
Error classification mirrors Go's priority: dns → timeout → tls → connect.
Output format matches Go's single-sample text layout.
Input: bare URL args or plain-text config file (same format as simple.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:07:44 +02:00

2.3 KiB

Python Port — Step 2: phases.py

Goal

Introduce the manual per-phase timing technique — the Python answer to Go's net/http/httptrace. A single self-contained script that hand-drives a raw socket for each URL and times each phase individually with time.perf_counter().

Why raw sockets

Python's urllib / httpx / requests give no per-phase callbacks, unlike Go's httptrace.ClientTrace. The only way to time DNS, TCP connect, TLS handshake, TTFB, and transfer independently is to drive the connection at the socket level:

  • socket.getaddrinfo() → DNS
  • sock.connect() → TCP
  • ssl.SSLContext.wrap_socket() → TLS (HTTPS only)
  • sock.sendall(request) + sock.recv() → TTFB
  • drain to EOF → Transfer

Input

  • Bare URL(s) as positional args: python phases.py https://example.com
  • Or a plain-text config file: python phases.py configs/all-ok.txt (detected by whether the first arg starts with http:// / https://)

Output

Mirrors Go's single-sample text layout for each URL:

https://example.com  (200)
  DNS lookup     :     18.21 ms
  TCP connect    :     10.12 ms
  TLS handshake  :     36.11 ms
  Server (TTFB)  :     21.95 ms
  Transfer       :      0.18 ms
  ─────────────────────────────
  Total          :     88.00 ms
  • TLS row omitted for http:// URLs.
  • On failure: header shows (FAILED), partial phases shown, error at the end.
  • Multiple URLs separated by a blank line.

Error classification

Mirrors Go's priority order (dns → timeout → tls → connect):

  • socket.gaierror"dns"
  • socket.timeout / TimeoutError"timeout" (regardless of phase)
  • ssl.SSLError or OSError during TLS wrap → "tls"
  • ConnectionRefusedError / other OSError during connect → "connect"
  • Error after first byte → "transfer"

Partial phases are preserved on failure (same invariant as Go).

Known limitations (documented in usage doc)

  • No redirect following (3xx responses are reported with their raw status code).
  • Connection: close + read-to-EOF; no keep-alive, no HTTP/2.
  • Timeout hard-coded at 10 s (no --timeout flag — that's in the full version).
  • Bodies fully drained to get accurate Transfer timing.

Files

  • python/phases.py — the script
  • docs/usage/py-phases.md — user-facing doc
  • CHANGELOG.md entry