Classify network failures (dns/connect/timeout/tls) with distinct exit codes 2-5. Add --timeout (default 10s) via context.WithTimeout. Add --fail for exit 6 on HTTP status >= 400. Preserve partial phase timing up to the failure point. -n sampling continues on network failures, aggregating successes and reporting fail counts. JSON extended with succeeded/failed/errors fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.9 KiB
3.9 KiB
Step 5 — Failure Handling
What this step delivers
latprobe now handles all three real-world failure modes explicitly:
| Failure | Exit code | Behaviour |
|---|---|---|
| DNS resolution failure | 2 | Partial timing (DNS duration) shown; classified as dns |
| Connection failure (refused / unreachable) | 3 | Partial timing shown; classified as connect |
Timeout (exceeded --timeout) |
4 | Partial timing shown; classified as timeout |
| TLS handshake failure | 5 | Partial timing shown; classified as tls |
HTTP status ≥ 400 (with --fail) |
6 | Full timing shown, status annotated with ✗ |
HTTP status ≥ 400 (without --fail) |
0 | Full timing shown, status visible but exit is 0 |
With -n sampling, network failures do not abort the run — remaining samples continue and successful ones are aggregated normally. The failure count and cause are reported alongside the aggregate.
When multiple URLs or failure types occur, the process exits with the highest exit code encountered.
New flags
| Flag | Default | Description |
|---|---|---|
--timeout |
10s |
Per-request timeout (e.g. 500ms, 2s, 30s) |
--fail |
off | Exit with code 6 when any URL returns HTTP status ≥ 400 |
Examples
DNS failure
$ ./latprobe https://nonexistent.invalid; echo $?
https://nonexistent.invalid (FAILED)
✗ dns: dial tcp: lookup nonexistent.invalid: no such host
2
Connection refused
$ ./latprobe http://localhost:1; echo $?
http://localhost:1 (FAILED)
✗ connect: dial tcp [::1]:1: connect: connection refused
3
Timeout
$ ./latprobe --timeout 1s https://example.com:81; echo $?
https://example.com:81 (FAILED)
✗ timeout: context deadline exceeded
4
HTTP error — default (exit 0)
$ ./latprobe https://httpbin.org/status/500; echo $?
https://httpbin.org/status/500 (500)
DNS lookup : 27.75 ms
TCP connect : 114.47 ms
TLS handshake : 245.59 ms
Server (TTFB) : 113.06 ms
Transfer : 0.21 ms
─────────────────────────────
Total : 502.05 ms
0
HTTP error — with --fail (exit 6)
$ ./latprobe --fail https://httpbin.org/status/404; echo $?
https://httpbin.org/status/404 (404 ✗)
DNS lookup : 3.35 ms
...
Total : 476.01 ms
6
Mixed sampling (some network failures)
$ ./latprobe -n 5 https://flaky-host.example.com; echo $?
https://flaky-host.example.com (200, 3 samples, 2 failed)
min avg max
DNS lookup : 5.00 ms 5.10 ms 5.20 ms
...
─────────────────────────────────────────────────
Total : 80.00 ms 85.00 ms 92.00 ms
✗ 2 × connect: connection refused
3
JSON output with failure
$ ./latprobe --json https://nonexistent.invalid | jq .
[
{
"url": "https://nonexistent.invalid",
"status": 0,
"succeeded": 0,
"failed": 1,
"errors": [
{
"phase": "dns",
"count": 1,
"message": "dial tcp: lookup nonexistent.invalid: no such host"
}
]
}
]
JSON schema changes
jsonEntry now includes:
{
"url": "https://example.com",
"status": 200,
"succeeded": 3,
"failed": 2,
"phases": { ... },
"errors": [
{ "phase": "connect", "count": 2, "message": "connection refused" }
]
}
phasesis omitted whensucceeded == 0errorsis omitted whenfailed == 0statusis 0 when no sample reached a response
Exit code reference
| Code | Meaning |
|---|---|
| 0 | All probes succeeded |
| 1 | Usage error (no URLs, bad flags) |
| 2 | DNS resolution failure |
| 3 | Connection failure |
| 4 | Timeout |
| 5 | TLS handshake failure |
| 6 | HTTP status ≥ 400 (only with --fail) |