httptap 0.6.2__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {httptap-0.6.2 → httptap-0.7.0}/PKG-INFO +118 -57
  2. {httptap-0.6.2 → httptap-0.7.0}/README.md +109 -53
  3. {httptap-0.6.2 → httptap-0.7.0}/httptap/__init__.py +4 -0
  4. {httptap-0.6.2 → httptap-0.7.0}/httptap/_pkgmeta.py +15 -2
  5. {httptap-0.6.2 → httptap-0.7.0}/httptap/analyzer.py +78 -16
  6. httptap-0.7.0/httptap/cli.py +1030 -0
  7. {httptap-0.6.2 → httptap-0.7.0}/httptap/constants.py +10 -2
  8. {httptap-0.6.2 → httptap-0.7.0}/httptap/exporter.py +22 -1
  9. {httptap-0.6.2 → httptap-0.7.0}/httptap/formatters.py +44 -5
  10. httptap-0.7.0/httptap/http_client.py +1282 -0
  11. {httptap-0.6.2 → httptap-0.7.0}/httptap/implementations/dns.py +91 -24
  12. {httptap-0.6.2 → httptap-0.7.0}/httptap/implementations/timing.py +6 -2
  13. {httptap-0.6.2 → httptap-0.7.0}/httptap/implementations/tls.py +18 -22
  14. {httptap-0.6.2 → httptap-0.7.0}/httptap/interfaces.py +9 -2
  15. {httptap-0.6.2 → httptap-0.7.0}/httptap/models.py +13 -5
  16. httptap-0.7.0/httptap/otlp.py +191 -0
  17. httptap-0.7.0/httptap/prometheus.py +122 -0
  18. httptap-0.7.0/httptap/py.typed +0 -0
  19. {httptap-0.6.2 → httptap-0.7.0}/httptap/render.py +29 -5
  20. {httptap-0.6.2 → httptap-0.7.0}/httptap/request_executor.py +12 -6
  21. {httptap-0.6.2 → httptap-0.7.0}/httptap/slo.py +34 -0
  22. {httptap-0.6.2 → httptap-0.7.0}/httptap/tls_inspector.py +77 -13
  23. httptap-0.7.0/httptap/utils.py +591 -0
  24. {httptap-0.6.2 → httptap-0.7.0}/httptap/visualizer.py +28 -6
  25. {httptap-0.6.2 → httptap-0.7.0}/pyproject.toml +44 -8
  26. {httptap-0.6.2 → httptap-0.7.0}/pyproject.toml.orig +38 -7
  27. httptap-0.6.2/httptap/cli.py +0 -604
  28. httptap-0.6.2/httptap/http_client.py +0 -715
  29. httptap-0.6.2/httptap/utils.py +0 -358
  30. {httptap-0.6.2 → httptap-0.7.0}/LICENSE +0 -0
  31. {httptap-0.6.2 → httptap-0.7.0}/httptap/implementations/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httptap
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: HTTP request visualizer with detailed timing breakdown (DNS → TCP → TLS → HTTP)
5
5
  Keywords: http,https,performance,timing,dns,tls,ssl,networking,monitoring,diagnostics,waterfall,curl,httpx
6
6
  Author: Sergei Ozeranskii
@@ -14,13 +14,13 @@ Classifier: Topic :: System :: Networking :: Monitoring
14
14
  Classifier: Topic :: Software Development :: Testing
15
15
  Classifier: Topic :: Utilities
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
21
20
  Classifier: Programming Language :: Python :: 3.14
22
21
  Classifier: Programming Language :: Python :: 3.15
23
22
  Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
24
24
  Classifier: Environment :: Console
25
25
  Classifier: Operating System :: OS Independent
26
26
  Classifier: Operating System :: POSIX
@@ -28,17 +28,22 @@ Classifier: Operating System :: MacOS
28
28
  Classifier: Operating System :: Microsoft :: Windows
29
29
  Classifier: Typing :: Typed
30
30
  Requires-Dist: httpx[http2,socks]>=0.28.1
31
+ Requires-Dist: httpcore>=1.0.0
31
32
  Requires-Dist: rich>=14.3.2
32
- Requires-Dist: dnspython>=2.8.0
33
+ Requires-Dist: cryptography>=50.0.2
33
34
  Requires-Dist: argcomplete>=3.7.2 ; extra == 'completion'
35
+ Requires-Dist: opentelemetry-api>=1.30.0 ; extra == 'otel'
36
+ Requires-Dist: opentelemetry-sdk>=1.30.0 ; extra == 'otel'
37
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.30.0 ; extra == 'otel'
34
38
  Maintainer: Sergei Ozeranskii
35
- Requires-Python: >=3.10
39
+ Requires-Python: >=3.11
36
40
  Project-URL: Homepage, https://github.com/ozeranskii/httptap
37
41
  Project-URL: Documentation, https://docs.httptap.dev
38
42
  Project-URL: Repository, https://github.com/ozeranskii/httptap.git
39
43
  Project-URL: Issues, https://github.com/ozeranskii/httptap/issues
40
44
  Project-URL: Changelog, https://github.com/ozeranskii/httptap/blob/main/CHANGELOG.md
41
45
  Provides-Extra: completion
46
+ Provides-Extra: otel
42
47
  Description-Content-Type: text/markdown
43
48
 
44
49
  <p align="center">
@@ -160,13 +165,21 @@ performance baselines.
160
165
  data is unavailable).
161
166
  - **All HTTP methods** – GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS with request body support.
162
167
  - **Request body support** – send JSON, XML, or any data inline or from file with automatic Content-Type detection.
163
- - **IPv4/IPv6 aware** – the resolver and TLS inspector report both the address and its family.
168
+ - **IPv4/IPv6 aware** – the resolver and TLS inspector report both the address and its family; `-4`/`-6` restrict
169
+ resolution to one family, and `--resolve HOST:PORT:ADDR` pins a host to an address while keeping the original `Host`
170
+ header and TLS SNI.
164
171
  - **TLS insights** – certificate CN, SANs, issuer, serial, validity window and expiry countdown, plus cipher suite and
165
172
  protocol version, are captured automatically from the live connection (no extra handshake).
166
173
  - **Multiple output modes** – rich waterfall view, compact single-line summaries, or `--metrics-only` for scripting.
167
- - **JSON export** – persist full step data (including redirect chains) for later processing.
168
- - **SLO threshold checking** – `--slo total=500,ttfb=200` gates CI jobs, cron probes, and readiness checks on per-phase
169
- latency budgets; non-zero exit on violation while still rendering the full report.
174
+ - **JSON export** – persist full step data (including redirect chains) for later processing, or stream it to stdout with
175
+ `--json -`.
176
+ - **Prometheus and OpenTelemetry export** – `--prometheus PATH` writes a node_exporter textfile; `--otlp ENDPOINT` sends
177
+ per-phase spans to an OTLP/HTTP collector (requires `httptap[otel]`).
178
+ - **SLO threshold checking** – `--slo total=500,ttfb=200` (or thresholds read from a file with `--slo-file`) gates CI
179
+ jobs, cron probes, and readiness checks on per-phase latency budgets; non-zero exit on violation while still rendering
180
+ the full report.
181
+ - **Scriptable exit codes** – `-f/--fail` exits `22` on HTTP 4xx/5xx responses, and SLO violations, network errors, and
182
+ the redirect limit each have their own [exit code](#exit-codes).
170
183
  - **Extensible** – clean Protocol interfaces for DNS, TLS, timing, visualization, and export so you can plug in custom
171
184
  behavior.
172
185
 
@@ -203,7 +216,7 @@ performance baselines.
203
216
 
204
217
  ## Requirements
205
218
 
206
- - Python 3.10-3.15 (CPython)
219
+ - Python 3.11-3.15 (CPython)
207
220
  - macOS, Linux, or Windows (tested on CPython)
208
221
  - No system dependencies beyond standard networking
209
222
  - Code must follow the Google Python Style Guide (docstrings, formatting). See
@@ -299,6 +312,20 @@ If you installed httptap via `pip` or `uv`, you need to install the optional com
299
312
 
300
313
  **Note:** The global activation script provides argument completions for bash and zsh only. Other shells are not covered by the script and must be configured separately.
301
314
 
315
+ #### Isolated installs (`uv tool`, `pipx`)
316
+
317
+ `uv tool install` and `pipx install` put only the `httptap` command on your `PATH`, not argcomplete's helper scripts. Expose them at install time, then register completion for `httptap` in your shell startup file (e.g. `~/.bashrc` or `~/.zshrc`):
318
+
319
+ ```shell
320
+ uv tool install --with-executables-from argcomplete "httptap[completion]"
321
+ # or
322
+ pipx install --include-resources-from argcomplete "httptap[completion]"
323
+
324
+ eval "$(register-python-argcomplete httptap)"
325
+ ```
326
+
327
+ Older pipx releases without `--include-resources-from` can use `--include-deps` instead.
328
+
302
329
  #### Usage Examples
303
330
 
304
331
  Once completions are installed, you can use `Tab` to autocomplete commands and options:
@@ -306,7 +333,7 @@ Once completions are installed, you can use `Tab` to autocomplete commands and o
306
333
  ```shell
307
334
  # Complete command options
308
335
  httptap --<TAB>
309
- # Shows: --method, --data, --follow, --timeout, --no-http2, --ignore-ssl, --cacert, --proxy, --header, --compact, --metrics-only, --json, --version, --help
336
+ # Shows: --help --version --request --method --data --location --follow --max-time --timeout --no-http2 --http1.1 --fail --ipv4 --ipv6 --resolve --insecure --ignore-ssl --cacert --ca-bundle --proxy --header --compact --metrics-only --json --prometheus --otlp --slo --slo-file
310
337
 
311
338
  # Complete after typing partial option
312
339
  httptap --fol<TAB>
@@ -339,7 +366,7 @@ httptap https://httpbin.io/post --data '{"name": "John", "email": "john@example.
339
366
 
340
367
  **Note:** When `--data` is provided without `--method`, httptap automatically switches to POST (similar to curl).
341
368
 
342
- **Curl-compatible flags:** httptap accepts the most common curl syntax, so you can often replace `curl` with `httptap` directly. Aliases include `-X/--request` for `--method`, `-L/--location` for `--follow`, `-m/--max-time` for `--timeout`, `-k/--insecure` for `--ignore-ssl`, `-x` for `--proxy`, and `--http1.1` for `--no-http2`. (Not every curl option is supported—stick to these shared flags when swapping commands.)
369
+ **Curl-compatible flags:** httptap accepts the most common curl syntax, so you can often replace `curl` with `httptap` directly. Aliases include `-X/--request` for `--method`, `-L/--location` for `--follow`, `-m/--max-time` for `--timeout`, `-k/--insecure` for `--ignore-ssl`, `-x` for `--proxy`, and `--http1.1` for `--no-http2`. `-f/--fail`, `-4/--ipv4`, `-6/--ipv6`, and `--resolve HOST:PORT:ADDR` use the same names as in curl. (Not every curl option is supported—stick to these shared flags when swapping commands.)
343
370
 
344
371
  Load data from file:
345
372
 
@@ -384,6 +411,9 @@ httptap \
384
411
  https://httpbin.io/bearer
385
412
  ```
386
413
 
414
+ Header names must be HTTP tokens and values printable ASCII (spaces and tabs allowed). Anything else, such as CR/LF or
415
+ non-ASCII text, is rejected with exit code `64` before any request is made.
416
+
387
417
  ### Redirects and JSON Export
388
418
 
389
419
  Follow redirect chains and dump metrics to JSON:
@@ -392,6 +422,10 @@ Follow redirect chains and dump metrics to JSON:
392
422
  httptap --follow --json out/report.json https://httpbin.io/redirect/2
393
423
  ```
394
424
 
425
+ A redirect to a URL that cannot be requested (invalid port, missing host, non-HTTP scheme) ends the chain with a failed
426
+ step, `Invalid redirect target: …`, and exit code `75`. Credentials in `Location` URLs are masked in the output and JSON
427
+ export.
428
+
395
429
  ### Output Modes
396
430
 
397
431
  Collect compact (single-line) timings suitable for logs:
@@ -450,7 +484,9 @@ httptap --proxy "" https://httpbin.io/get
450
484
 
451
485
  The output and JSON export include the proxy URI and its source so you
452
486
  can confirm what path was used (e.g., `(from arg --proxy)`,
453
- `(from env HTTPS_PROXY)`, `(bypassed by env no_proxy)`).
487
+ `(from env HTTPS_PROXY)`, `(bypassed by env no_proxy)`). Proxy credentials
488
+ are masked (`http://user:****@proxy:3128`). A malformed `--proxy` value is
489
+ rejected with exit code `64`.
454
490
 
455
491
  ---
456
492
 
@@ -499,23 +535,28 @@ Full specification, evaluation rules, and recipes:
499
535
 
500
536
  ## Environment Variables
501
537
 
502
- httptap reads the following environment variables at runtime. All of them are
503
- overridable via CLI flags, and the actual source used for each request is
504
- recorded in the output and JSON export.
538
+ httptap reads the following environment variables at runtime. The proxy
539
+ variables can be overridden with `-x/--proxy` or ignored with `--proxy ""`, and
540
+ the proxy source used for each request is recorded in the output and JSON
541
+ export. The color variables have no CLI equivalent.
505
542
 
506
543
  | Variable | Purpose | Overridden by |
507
544
  |---------------------------------------|--------------------------------------------------------------------------------------------------------------|-----------------------|
508
545
  | `HTTP_PROXY` / `http_proxy` | Proxy URL used for `http://` targets. | `-x/--proxy` |
509
546
  | `HTTPS_PROXY` / `https_proxy` | Proxy URL used for `https://` targets. | `-x/--proxy` |
510
547
  | `ALL_PROXY` / `all_proxy` | Fallback proxy URL when scheme-specific variables are unset. | `-x/--proxy` |
511
- | `NO_PROXY` / `no_proxy` | Comma-separated exclusion list (supports `*`, leading `.`, exact matches). Bypassed entries connect direct. | `--proxy ""` |
548
+ | `NO_PROXY` / `no_proxy` | Comma-separated exclusion list: `*` matches every host, `example.com` the host and its subdomains, `.example.com` subdomains only. No CIDR ranges. Bypassed hosts connect direct. | `-x/--proxy`, `--proxy ""` |
512
549
  | `NO_COLOR` | Disables ANSI colors in all Rich output (honors the [NO_COLOR](https://no-color.org) convention). | — |
513
550
  | `FORCE_COLOR` | Forces colored output even when stdout is not a TTY (Rich convention). | — |
514
551
  | `TERM=dumb` | Rich downgrades to plain-text rendering. | — |
515
552
 
516
- > Precedence for proxy configuration: explicit `-x/--proxy` → `--proxy ""`
517
- > (disables env) → `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` (scheme-matching) →
518
- > `NO_PROXY` exclusion → direct connection.
553
+ > Precedence for proxy configuration: explicit `-x/--proxy` or `--proxy ""`
554
+ > (disables env) → `NO_PROXY` exclusion (direct connection) → the variable
555
+ > matching the URL scheme (`HTTPS_PROXY` or `HTTP_PROXY`) → `ALL_PROXY` →
556
+ > direct connection. Lowercase variables take priority over uppercase ones.
557
+ > A proxy variable without a scheme (`proxy.local:3128`) is treated as
558
+ > `http://`; one that is not a valid proxy URL fails the request with a
559
+ > network error (exit code `75`).
519
560
 
520
561
  ---
521
562
 
@@ -528,11 +569,20 @@ shell pipelines, CI jobs, and systemd services.
528
569
  |:-----:|-------------------------|------------------------------------------------------------|
529
570
  | `0` | `EX_OK` | Success. |
530
571
  | `4` | — | SLO threshold violation (request succeeded but too slow). |
572
+ | `22` | — | HTTP 4xx/5xx response when `-f` / `--fail` is used. |
573
+ | `47` | — | Maximum redirects followed. |
531
574
  | `64` | `EX_USAGE` | Invalid command-line arguments. |
532
575
  | `70` | `EX_SOFTWARE` | Internal error (unexpected exception, bug). |
576
+ | `73` | `EX_CANTCREAT` | `--json` output file could not be written. |
533
577
  | `75` | `EX_TEMPFAIL` | Network / TLS error (partial output may still be rendered). |
534
578
  | `128 + N` | Signal offset | Killed by signal `N` (e.g., `130` for `SIGINT` / Ctrl-C). |
535
579
 
580
+ When several conditions apply, the highest-priority code wins:
581
+ `70` > `47` > `75` > `73` > `22` > `4` > `0`. Invalid arguments (`64`) are
582
+ reported before any request is made. See
583
+ [SLO exit codes](https://docs.httptap.dev/usage/slo/#exit-codes) for the full
584
+ precedence table.
585
+
536
586
  Example — fail a CI job only on usage errors, tolerating transient network
537
587
  issues:
538
588
 
@@ -549,26 +599,20 @@ fi
549
599
 
550
600
  ## Releasing
551
601
 
552
- ### Prerequisites
602
+ Releases are cut by the manually triggered **Release** workflow (GitHub Actions → **Release** → **Run workflow**) with
603
+ either an exact version (e.g., `0.3.0`) or a `patch`/`minor`/`major` bump:
553
604
 
554
- - GitHub Environment `pypi` must be configured in repository settings
555
- - PyPI Trusted Publishing configured for `ozeranskii/httptap`
605
+ 1. **Prepare** – bumps the version with `uv version`, refreshes `uv.lock`, prepends the `git-cliff` changelog entry to
606
+ `CHANGELOG.md`, and creates a gitsign-signed release commit and tag locally. Nothing is pushed yet.
607
+ 2. **Build** – runs the full test suite on the unpushed tag, builds the wheel and sdist, generates the SBOMs (CycloneDX,
608
+ SPDX), OpenVEX document, and man page, and attests build provenance.
609
+ 3. **Push** – only after the build and attestation succeed, fast-forwards `main` to the release commit and pushes the
610
+ tag. If `main` moved during the release, the push fails and nothing is published.
611
+ 4. **Publish** – uploads to TestPyPI and then PyPI via Trusted Publishing (OIDC), pushes the signed multi-arch container
612
+ image to GHCR, and creates the GitHub Release with the wheel, sdist, SBOM, VEX, and man page.
556
613
 
557
- ### Steps
558
-
559
- 1. Trigger the **Release** workflow from GitHub Actions:
560
- - Provide exact version (e.g., `0.3.0`), OR
561
- - Select bump type: `patch`, `minor`, or `major`
562
- 2. The workflow will:
563
- - Update version in `pyproject.toml` using `uv version`
564
- - Generate changelog with `git-cliff` and update `CHANGELOG.md`
565
- - Commit changes and create a git tag
566
- - Run full test suite on the tagged version
567
- - Build wheel and source distribution
568
- - Generate SBOM in CycloneDX and SPDX formats via Syft
569
- - Attach the current OpenVEX document (`.vex/httptap.openvex.json`)
570
- - Publish to PyPI via Trusted Publishing (OIDC)
571
- - Create GitHub Release with wheel, sdist, SBOM, and VEX assets
614
+ Prerequisites (GitHub environments, Trusted Publishing, deploy key) and job details are in the
615
+ [release process documentation](https://docs.httptap.dev/development/release/).
572
616
 
573
617
  ---
574
618
 
@@ -585,6 +629,9 @@ The redirect summary includes a total row:
585
629
 
586
630
  ```json
587
631
  {
632
+ "schema_version": 1,
633
+ "httptap_version": "0.6.3",
634
+ "timestamp": "2026-09-18T08:00:00Z",
588
635
  "initial_url": "https://httpbin.io/redirect/2",
589
636
  "total_steps": 3,
590
637
  "steps": [
@@ -613,14 +660,14 @@ The redirect summary includes a total row:
613
660
  "tls_version": "TLSv1.2",
614
661
  "tls_cipher": "ECDHE-RSA-AES128-GCM-SHA256",
615
662
  "cert_cn": "httpbin.io",
616
- "cert_days_left": 143,
663
+ "cert_days_left": 41,
617
664
  "cert_sans": ["httpbin.io", "*.httpbin.io"],
618
665
  "cert_issuer": "WE1",
619
666
  "cert_serial": "05BB0F0AA84C8FECE0E72D805BA7A5D2B",
620
- "cert_not_before": "2025-04-01T00:00:00+00:00",
621
- "cert_not_after": "2025-09-01T00:00:00+00:00",
667
+ "cert_not_before": "2026-08-01T00:00:00+00:00",
668
+ "cert_not_after": "2026-10-30T00:00:00+00:00",
622
669
  "tls_verified": true,
623
- "tls_custom_ca": null,
670
+ "tls_custom_ca": false,
624
671
  "proxy_url": null,
625
672
  "proxy_source": null
626
673
  },
@@ -629,18 +676,19 @@ The redirect summary includes a total row:
629
676
  "bytes": 0,
630
677
  "content_type": null,
631
678
  "server": null,
632
- "date": "2025-10-23T19:20:36+00:00",
679
+ "date": "2026-09-18T07:59:59+00:00",
633
680
  "location": "/relative-redirect/1",
634
681
  "headers": {
635
682
  "access-control-allow-credentials": "true",
636
683
  "access-control-allow-origin": "*",
637
684
  "location": "/relative-redirect/1",
638
- "date": "Thu, 23 Oct 2025 19:20:36 GMT",
685
+ "date": "Fri, 18 Sep 2026 07:59:59 GMT",
639
686
  "content-length": "0"
640
687
  }
641
688
  },
642
689
  "error": null,
643
690
  "note": null,
691
+ "redirect_limit_reached": false,
644
692
  "proxy": null
645
693
  },
646
694
  {
@@ -668,14 +716,14 @@ The redirect summary includes a total row:
668
716
  "tls_version": "TLSv1.2",
669
717
  "tls_cipher": "ECDHE-RSA-AES128-GCM-SHA256",
670
718
  "cert_cn": "httpbin.io",
671
- "cert_days_left": 143,
719
+ "cert_days_left": 41,
672
720
  "cert_sans": ["httpbin.io", "*.httpbin.io"],
673
721
  "cert_issuer": "WE1",
674
722
  "cert_serial": "05BB0F0AA84C8FECE0E72D805BA7A5D2B",
675
- "cert_not_before": "2025-04-01T00:00:00+00:00",
676
- "cert_not_after": "2025-09-01T00:00:00+00:00",
723
+ "cert_not_before": "2026-08-01T00:00:00+00:00",
724
+ "cert_not_after": "2026-10-30T00:00:00+00:00",
677
725
  "tls_verified": true,
678
- "tls_custom_ca": null,
726
+ "tls_custom_ca": false,
679
727
  "proxy_url": null,
680
728
  "proxy_source": null
681
729
  },
@@ -684,18 +732,19 @@ The redirect summary includes a total row:
684
732
  "bytes": 0,
685
733
  "content_type": null,
686
734
  "server": null,
687
- "date": "2025-10-23T19:20:36+00:00",
735
+ "date": "2026-09-18T07:59:59+00:00",
688
736
  "location": "/get",
689
737
  "headers": {
690
738
  "access-control-allow-credentials": "true",
691
739
  "access-control-allow-origin": "*",
692
740
  "location": "/get",
693
- "date": "Thu, 23 Oct 2025 19:20:36 GMT",
741
+ "date": "Fri, 18 Sep 2026 07:59:59 GMT",
694
742
  "content-length": "0"
695
743
  }
696
744
  },
697
745
  "error": null,
698
746
  "note": null,
747
+ "redirect_limit_reached": false,
699
748
  "proxy": null
700
749
  },
701
750
  {
@@ -723,14 +772,14 @@ The redirect summary includes a total row:
723
772
  "tls_version": "TLSv1.2",
724
773
  "tls_cipher": "ECDHE-RSA-AES128-GCM-SHA256",
725
774
  "cert_cn": "httpbin.io",
726
- "cert_days_left": 143,
775
+ "cert_days_left": 41,
727
776
  "cert_sans": ["httpbin.io", "*.httpbin.io"],
728
777
  "cert_issuer": "WE1",
729
778
  "cert_serial": "05BB0F0AA84C8FECE0E72D805BA7A5D2B",
730
- "cert_not_before": "2025-04-01T00:00:00+00:00",
731
- "cert_not_after": "2025-09-01T00:00:00+00:00",
779
+ "cert_not_before": "2026-08-01T00:00:00+00:00",
780
+ "cert_not_after": "2026-10-30T00:00:00+00:00",
732
781
  "tls_verified": true,
733
- "tls_custom_ca": null,
782
+ "tls_custom_ca": false,
734
783
  "proxy_url": null,
735
784
  "proxy_source": null
736
785
  },
@@ -739,18 +788,19 @@ The redirect summary includes a total row:
739
788
  "bytes": 389,
740
789
  "content_type": "application/json; charset=utf-8",
741
790
  "server": null,
742
- "date": "2025-10-23T19:20:37+00:00",
791
+ "date": "2026-09-18T08:00:00+00:00",
743
792
  "location": null,
744
793
  "headers": {
745
794
  "access-control-allow-credentials": "true",
746
795
  "access-control-allow-origin": "*",
747
796
  "content-type": "application/json; charset=utf-8",
748
- "date": "Thu, 23 Oct 2025 19:20:37 GMT",
797
+ "date": "Fri, 18 Sep 2026 08:00:00 GMT",
749
798
  "content-length": "389"
750
799
  }
751
800
  },
752
801
  "error": null,
753
802
  "note": null,
803
+ "redirect_limit_reached": false,
754
804
  "proxy": null
755
805
  }
756
806
  ],
@@ -795,6 +845,11 @@ analyzer = HTTPTapAnalyzer(dns_resolver=HardcodedDNS())
795
845
  steps = analyzer.analyze_url("https://httpbin.io")
796
846
  ```
797
847
 
848
+ `HardcodedDNS` overrides only `resolve()`, so httptap calls it instead of the inherited `resolve_all()` and connects to
849
+ that single address. Override `resolve_all()` as well to return several addresses for connection fallback, and raise
850
+ `DNSResolutionError` when a name cannot be resolved; see
851
+ [Protocol Interfaces](https://docs.httptap.dev/api/interfaces/#dnsresolver).
852
+
798
853
  ---
799
854
 
800
855
  ## Development
@@ -808,7 +863,13 @@ uv run ruff check
808
863
  uv run ruff format .
809
864
  ```
810
865
 
811
- Tests expect outbound network access; you can mock `SystemDNSResolver` / `SocketTLSInspector` when running offline.
866
+ The test suite does not need outbound network access: HTTP calls are mocked with `pytest-httpx`, and TLS and proxy tests use local servers.
867
+
868
+ The end-to-end tests in `tests/e2e` run the `httptap` CLI as a subprocess against local HTTP, TLS, proxy and OTLP servers, so they need no outbound network access either. A plain `uv run pytest` skips them; run them with:
869
+
870
+ ```shell
871
+ uv run --extra otel pytest tests/e2e --no-cov -n auto
872
+ ```
812
873
 
813
874
  ---
814
875
 
@@ -834,7 +895,7 @@ details.
834
895
 
835
896
  - Built on the shoulders of fantastic
836
897
  libraries: [httpx](https://www.python-httpx.org/), [httpcore](https://github.com/encode/httpcore),
837
- [dnspython](https://www.dnspython.org/), and [Rich](https://github.com/Textualize/rich).
898
+ [cryptography](https://cryptography.io/), and [Rich](https://github.com/Textualize/rich).
838
899
  - Inspired by the tooling ecosystem around web performance (e.g., DevTools waterfalls, `curl --trace`).
839
900
  - Special thanks to everyone who opens issues, shares ideas, or contributes patches.
840
901