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

69 lines
2.3 KiB
Markdown

# 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