Advanced Features¶
This guide covers advanced usage patterns and customization options for httptap.
Custom DNS Resolution¶
You can provide custom DNS resolver implementations by using the Python API. httptap always dials the resolved IP address (IPv4/IPv6) while preserving the original hostname for the Host header and TLS SNI. IPv6 literals are bracketed automatically, so custom resolvers only need to return the correct IP/family tuple.
from httptap import HTTPTapAnalyzer, SystemDNSResolver
class CustomDNSResolver(SystemDNSResolver):
"""Custom DNS resolver with hardcoded responses."""
def resolve(self, host: str, port: int, timeout: float):
# Override with custom logic
if host == "httpbin.io":
return "44.211.11.205", "IPv4", 0.1
return super().resolve(host, port, timeout)
# Use custom resolver
analyzer = HTTPTapAnalyzer(dns_resolver=CustomDNSResolver())
steps = analyzer.analyze_url("https://httpbin.io")
Because this subclass overrides only resolve(), httptap calls it rather than the inherited resolve_all(), so every host resolves to a single address and there is no fallback to the next address when a connection fails. Override resolve_all() as well to keep that fallback; see Address fallback with resolve_all(). Raise DNSResolutionError from a custom resolver when a name cannot be resolved, so the failure is reported as a network error rather than an internal one.
Custom TLS Inspection¶
httptap reads the TLS version, cipher, and certificate details directly from the live connection that served the response, so a custom TLSInspector does not replace that data. It is only a fallback: httptap calls it for an HTTPS request when the live connection exposes no TLS data and no proxy is in use, and its result then fills the TLS fields. The inspector receives the hostname, port, and remaining timeout and opens its own connection. Raise httptap.TLSInspectionError when inspection fails so the step is still reported without TLS details. The diagnostic probe that collects certificate details after a certificate verification failure always uses the built-in SocketTLSInspector, never a custom inspector.
from httptap import HTTPTapAnalyzer
from httptap.interfaces import TLSInspector
from httptap.models import NetworkInfo
class CustomTLSInspector:
"""Custom TLS inspector with extended certificate checks."""
def inspect(self, host: str, port: int, timeout: float) -> NetworkInfo:
# Custom TLS inspection logic
# Return: NetworkInfo with TLS version, cipher, and certificate data
...
analyzer = HTTPTapAnalyzer(tls_inspector=CustomTLSInspector())
Programmatic Usage¶
Use httptap as a Python library for integration into your applications.
Basic Analysis¶
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")
for step in steps:
print(f"URL: {step.url}")
print(f"Status: {step.response.status}")
print(f"Total time: {step.timing.total_ms:.2f}ms")
With Custom Headers¶
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
headers = {"Authorization": "Bearer token123", "Accept": "application/json"}
steps = analyzer.analyze_url("https://httpbin.io/bearer", headers=headers)
Following Redirects¶
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer(follow_redirects=True)
steps = analyzer.analyze_url("https://httpbin.io/redirect/3")
print(f"Total steps in redirect chain: {len(steps)}")
Each redirect hop follows the same header and method rules as the CLI: credential headers are not sent to a different origin, and 303 (or 301/302 after POST) switches to GET without a body. See --follow for details.
Sending Request Body¶
from httptap import HTTPTapAnalyzer
from httptap.constants import HTTPMethod
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url(
"https://httpbin.io/post",
method=HTTPMethod.POST,
content=b'{"key": "value"}',
headers={"Content-Type": "application/json"},
)
Ignoring TLS Verification¶
When troubleshooting staging environments or hosts with self-signed certificates, you can skip TLS validation:
The request still records TLS metadata, but certificate errors are suppressed so you can focus on the protocol flow. Only use this flag in trusted environments because it disables protection against man-in-the-middle attacks. The client relaxes many cipher and protocol requirements (weak hashes, older TLS versions, small DH groups) so that legacy endpoints are more likely to complete the handshake. Extremely deprecated algorithms that OpenSSL removes entirely (e.g., RC4, 3DES on some platforms) may still fail even in this mode.
Using Proxies¶
Direct requests through an outbound proxy (HTTP, HTTPS, SOCKS5/SOCKS5H):
Ignore all proxy environment variables and connect directly:
The Rich output and JSON export include the proxy URI and its source (e.g., (from arg --proxy), (from env HTTPS_PROXY), (bypassed by env no_proxy)) so you can confirm which path was used.
Proxy Protocols and DNS Resolution¶
httptap supports four proxy protocols, each with different DNS resolution behavior:
| Protocol | DNS Resolved By | Use Case |
|---|---|---|
socks5h:// | Proxy server | Privacy, corporate networks, access to internal DNS |
http:// | Proxy server | Standard HTTP proxies (CONNECT method) |
https:// | Proxy server | Encrypted connection to proxy |
socks5:// | Client (local) | When you need to control DNS resolution |
The h suffix in socks5h stands for "hostname" (a curl convention). With socks5h://, the hostname is sent to the proxy which resolves it. With socks5://, the client resolves DNS locally and sends the IP to the proxy.
With socks5://, when the host resolves to several addresses and the proxy reports that it cannot connect to one of them, httptap tries the next address. Failures to reach or authenticate with the proxy itself are not retried, since they would repeat for every address. IPv6 literal targets (https://[2001:db8::1]/) work with every proxy type.
Environment Variable Proxies¶
When no --proxy flag is provided, httptap checks environment variables:
no_proxy/NO_PROXY- Comma-separated list of hosts to bypass (lowercase takes priority)https_proxy/HTTPS_PROXY- Proxy for HTTPS requests (lowercase takes priority)http_proxy/HTTP_PROXY- Proxy for HTTP requests (lowercase takes priority)all_proxy/ALL_PROXY- Fallback proxy for all protocols
The --proxy flag always takes precedence over environment variables.
A variable without a scheme (proxy.internal:3128) is treated as an http:// proxy, as curl does. If the selected variable does not hold a valid proxy URL (an unsupported scheme, a missing host, an invalid port), the request fails with a network error (exit code 75) that names the variable and shows the URL with its password masked.
NO_PROXY patterns:
*- Bypass proxy for all hostsexample.com- The host itself and all of its subdomains.example.com- Subdomains of example.com only, not example.com itselfsub.example.com- sub.example.com and its subdomains
Matching is case-insensitive. CIDR ranges and port-specific entries are not supported.
Custom CA Bundles¶
For internal endpoints signed by a private CA, supply a PEM bundle with --cacert:
CLI output will show TLS CA: custom bundle to indicate the non-system trust store was used. JSON exports include network.tls_custom_ca: true so downstream tools can detect custom trust. The flag is mutually exclusive with --ignore-ssl.
Custom Request Executors¶
For fully customized behavior you can provide your own request executor. Executors receive all parameters packaged inside RequestOptions, so new flags added by httptap remain backward compatible. Forward every field your transport supports; dropping one silently changes the request (for example, ignoring method and content turns a POST into a body-less GET, and ignoring proxy or ca_bundle_path drops the --proxy or --cacert setting).
from httptap import HTTPTapAnalyzer, RequestExecutor, RequestOptions, RequestOutcome
from httptap.http_client import make_request
class RecordingExecutor(RequestExecutor):
def __init__(self) -> None:
self.last_options: RequestOptions | None = None
def execute(self, options: RequestOptions) -> RequestOutcome:
self.last_options = options
# Call the built-in client (or your preferred HTTP library)
timing, network, response = make_request(
options.url,
options.timeout,
deadline=options.deadline,
method=options.method,
content=options.content,
http2=options.http2,
verify_ssl=options.verify_ssl,
ca_bundle_path=options.ca_bundle_path,
proxy=options.proxy,
noproxy=options.noproxy,
dns_resolver=options.dns_resolver,
tls_inspector=options.tls_inspector,
timing_collector=options.timing_collector,
headers=options.headers,
)
return RequestOutcome(timing=timing, network=network, response=response)
executor = RecordingExecutor()
analyzer = HTTPTapAnalyzer(request_executor=executor)
analyzer.analyze_url("https://httpbin.io/get", headers={"X-Debug": "1"})
print(executor.last_options.headers) # {'X-Debug': '1'}
force_new_connection is deprecated and ignored, so it is not forwarded. To wrap the default behavior instead of re-implementing it, delegate to HTTPClientRequestExecutor().execute(options). Raise httptap.http_client.HTTPClientError for transport failures so the analyzer records them as network errors (exit code 75) with partial data; any other exception is reported as an internal error (exit code 70).
Custom Visualization¶
Create your own visualization by implementing the Visualizer protocol.
from httptap.models import StepMetrics
class CustomVisualizer:
"""Custom visualizer for request steps."""
def render(self, step: StepMetrics) -> None:
print(f"Step {step.step_number}: {step.timing.total_ms}ms")
# Use custom visualizer
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")
visualizer = CustomVisualizer()
for step in steps:
visualizer.render(step)
Custom Export Formats¶
Implement custom export formats beyond JSON.
from collections.abc import Sequence
from httptap.models import StepMetrics
import csv
class CSVExporter:
"""Export request data to CSV format."""
def export(self, steps: Sequence[StepMetrics], initial_url: str, output_path: str) -> None:
with open(output_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["url", "status", "dns_ms", "connect_ms", "tls_ms", "ttfb_ms", "total_ms"])
for step in steps:
writer.writerow(
[
step.url,
step.response.status,
step.timing.dns_ms,
step.timing.connect_ms,
step.timing.tls_ms,
step.timing.ttfb_ms,
step.timing.total_ms,
]
)
# Usage
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")
exporter = CSVExporter()
exporter.export(steps, "https://httpbin.io", "output.csv")
Performance Monitoring¶
Use httptap for continuous performance monitoring.
import time
from httptap import HTTPTapAnalyzer
def monitor_endpoint(url: str, interval: int = 60):
"""Monitor endpoint every interval seconds."""
analyzer = HTTPTapAnalyzer()
while True:
steps = analyzer.analyze_url(url)
step = steps[0]
# Log metrics
print(
f"{time.strftime('%Y-%m-%d %H:%M:%S')} - "
f"TTFB: {step.timing.ttfb_ms:.2f}ms, "
f"Total: {step.timing.total_ms:.2f}ms, "
f"Status: {step.response.status}"
)
time.sleep(interval)
# Monitor API endpoint every minute
monitor_endpoint("https://httpbin.io/status/200", interval=60)
Batch Analysis¶
Analyze multiple URLs concurrently.
from concurrent.futures import ThreadPoolExecutor
from httptap import HTTPTapAnalyzer
def analyze_url(url: str):
"""Analyze a single URL."""
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url(url)
return url, steps[0].timing.total_ms
# List of URLs to analyze
urls = ["https://httpbin.io", "https://httpbin.io/delay/1", "https://httpbin.io/gzip"]
# Analyze concurrently
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(analyze_url, urls))
# Print results
for url, total_ms in results:
print(f"{url}: {total_ms:.2f}ms")
Error Handling¶
Handle errors gracefully when analyzing URLs. has_error is set only when the request itself failed (DNS, connection, TLS, or timeout); an HTTP 4xx/5xx response is a completed step, so check response.status separately.
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://nonexistent.invalid")
step = steps[0]
if step.has_error:
print(f"Error: {step.error}")
elif step.response.status >= 400:
print(f"HTTP error: {step.response.status}")
else:
print(f"Status: {step.response.status}")
Integration with Testing Frameworks¶
Use httptap in your test suites to verify performance requirements.
import pytest
from httptap import HTTPTapAnalyzer
def test_api_response_time():
"""Test that API responds within acceptable time."""
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io/delay/0")
# Assert TTFB is under 500ms
assert steps[0].timing.ttfb_ms < 500, f"TTFB too high: {steps[0].timing.ttfb_ms}ms"
# Assert total time is under 1 second
assert steps[0].timing.total_ms < 1000, f"Total time too high: {steps[0].timing.total_ms}ms"
def test_tls_configuration():
"""Verify TLS configuration meets security standards."""
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")
# Assert TLS 1.2 or higher
assert steps[0].network.tls_version in ["TLSv1.2", "TLSv1.3"], (
f"Insecure TLS version: {steps[0].network.tls_version}"
)
# Assert certificate is valid for at least 30 days
assert steps[0].network.cert_days_left > 30, f"Certificate expiring soon: {steps[0].network.cert_days_left} days"
Environment-Specific Configuration¶
Configure httptap differently for various environments.
import os
from httptap import HTTPTapAnalyzer
# Environment-specific settings
config = {
"production": {
"timeout": 30,
"follow_redirects": True,
},
"staging": {
"timeout": 60,
"follow_redirects": True,
},
"development": {
"timeout": 120,
"follow_redirects": False,
},
}
env = os.getenv("ENVIRONMENT", "development")
settings = config[env]
analyzer = HTTPTapAnalyzer(
timeout=settings["timeout"],
follow_redirects=settings["follow_redirects"],
)
steps = analyzer.analyze_url("https://httpbin.io/status/200")
Debugging Tips¶
Enable Detailed Logging¶
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")
Inspect Raw HTTP Traffic¶
from httptap import HTTPTapAnalyzer
analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")
# Inspect response headers
step = steps[0]
print("Response headers:")
for key, value in step.response.headers.items():
print(f" {key}: {value}")
What's Next?¶
-
Detailed interface documentation
-
Extend httptap and contribute
-
How releases work