Back to Blog
Python

Using httpx with Proxy, SSL, and HTTP/2

python httpx proxy ssl and http2: Learn how to configure httpx with a proxy, SSL/TLS verification, and HTTP/2 support. Includes code examples, pitfalls, and production...

httpxproxySSL/TLSHTTP/2Python networking
Diagram of an HTTP client connecting through a proxy with a TLS handshake and HTTP/2 multiplexed streams.

When you need to route HTTP requests through a proxy, enforce TLS certificate validation, and take advantage of HTTP/2 multiplexing, httpx is one of the few Python clients that can handle all three cleanly. This article explains how to combine python httpx proxy ssl and http2 settings in a single client, what each option actually does, and where the interaction between these features tends to break.

Setting Up a Proxy in httpx

The proxy parameter on httpx.Client accepts a URL string or a dictionary of URL strings for different protocols. For a single proxy that handles both HTTP and HTTPS, pass a string like http://proxy.example.com:8080. If you need separate proxies for HTTP and HTTPS, use a dictionary:

import httpx proxy = { "http": "http://http-proxy.example.com:8080", "https": "http://https-proxy.example.com:8080", } with httpx.Client(proxy=proxy) as client: response = client.get("https://example.com")

When you use a proxy, the client sends an absolute-form request URI to the proxy instead of the origin-form. The proxy then forwards the request to the target server. For HTTPS requests, the client first sends a CONNECT request to the proxy, which establishes a tunnel through which the TLS handshake occurs. This means the proxy never sees the plaintext traffic, only the encrypted bytes.

If you rely on environment variables like HTTP_PROXY or HTTPS_PROXY, httpx will pick them up automatically unless you explicitly set proxy or trust_env=False. In most production setups, you'll want to set the proxy explicitly so that behavior is predictable.

Controlling SSL/TLS Verification

By default, httpx verifies SSL certificates using the system CA bundle. The verify parameter controls this behavior. You can pass a path to a custom CA bundle or a directory containing certificates, or you can set verify=False to disable verification entirely.

import httpx # Use a custom CA bundle with httpx.Client(verify="/path/to/ca.pem") as client: response = client.get("https://internal.example.com") # Disable verification (not recommended for production) with httpx.Client(verify=False) as client: response = client.get("https://self-signed.example.com")

Disabling verification is tempting when you're dealing with self-signed certificates in a test environment, but it opens the door to man-in-the-middle attacks. Even in development, it's better to add the self-signed certificate to a local CA bundle and pass that bundle via verify. This keeps your code honest and prevents accidental production outages when someone forgets to flip the flag back.

When a proxy is involved, SSL verification happens in two places. First, the client verifies the proxy's TLS certificate if the proxy URL uses https://. Second, it verifies the target server's certificate after the tunnel is established. The verify parameter applies to both. If your proxy uses a self-signed certificate, you'll need to include that certificate in the CA bundle as well.

Enabling HTTP/2 Support

httpx supports HTTP/2, but it's not enabled by default. To use it, install the h2 package and set http2=True on the client:

import httpx with httpx.Client(http2=True) as client: response = client.get("https://example.com")

When HTTP/2 is enabled, httpx negotiates the protocol during the TLS handshake via ALPN. If the server supports HTTP/2, the connection uses it; otherwise, it falls back to HTTP/1.1. This negotiation happens transparently, so you don't need to check the protocol version in your code unless you specifically want to log it.

HTTP/2 multiplexes multiple requests over a single connection, which reduces latency when you're making many concurrent requests to the same host. However, the benefit is only visible if you're actually using concurrency, such as with httpx.AsyncClient or multiple threads sharing a single client instance. For a simple sequential request loop, HTTP/2 won't show a measurable difference.

Combining Proxy, SSL, and HTTP/2 in One Client

You can set all three options on the same client. Here's a complete example that uses a proxy, verifies certificates against a custom CA bundle, and enables HTTP/2:

import httpx proxy = "http://proxy.example.com:8080" ca_bundle = "/etc/ssl/certs/ca-certificates.crt" with httpx.Client( proxy=proxy, verify=ca_bundle, http2=True, ) as client: response = client.get("https://api.example.com/data") print(response.status_code)

When you combine these features, the request flow becomes:

  1. The client connects to the proxy and, if the proxy URL is HTTPS, verifies the proxy's certificate.
  2. For an HTTPS target, the client sends a CONNECT request to establish a tunnel.
  3. Through the tunnel, the client performs a TLS handshake with the target server, verifying its certificate against the CA bundle.
  4. ALPN negotiates HTTP/2 if both sides support it.
  5. The request is sent over the multiplexed connection.

All of this happens automatically. The verify setting applies to both the proxy and the target server, so make sure your CA bundle includes both certificates if they are not publicly trusted.

Common Failures When These Features Interact

Several subtle issues appear when proxy, SSL, and HTTP/2 are combined.

Proxy certificate verification fails. If your proxy uses a self-signed certificate and you pass verify pointing only to the target server's CA, the client will reject the proxy's certificate. The fix is to add the proxy's certificate to the same CA bundle, or use a separate verify setting per connection—but httpx doesn't support per-connection verification. You'll need to create a combined CA bundle.

HTTP/2 over proxy with CONNECT. Some proxies do not correctly handle ALPN negotiation through a CONNECT tunnel. They may strip the ALPN extension or force HTTP/1.1. If you see httpx.RemoteProtocolError or the response appears to be HTTP/1.1 despite http2=True, the proxy is likely interfering. You can check the actual protocol version by inspecting response.http_version. If the proxy is the culprit, you may need to disable HTTP/2 for that route or use a different proxy.

Environment proxy variables override explicit settings. If you set proxy explicitly, httpx ignores environment variables. But if you leave proxy=None and trust_env=True (the default), the client will pick up HTTP_PROXY and HTTPS_PROXY. This can cause surprising behavior when you think you've disabled a proxy but the environment still has one set. Always set proxy explicitly or set trust_env=False when you need deterministic behavior.

Certificate verification with a proxy that does MITM inspection. Corporate proxies often terminate TLS and re-issue certificates signed by an internal CA. In that case, you must add that internal CA to your verify bundle, or you'll get a certificate verification error. The target server's certificate chain will be different from what you'd see without the proxy.

Performance and Security Tradeoffs

Enabling HTTP/2 can reduce latency for concurrent requests, but it also adds memory overhead because the client maintains a connection pool with multiplexed streams. If your workload is mostly sequential, HTTP/2 may not be worth the extra complexity. Measure your actual request pattern before deciding.

Using a proxy adds a network hop, which increases latency. For high-throughput services, a proxy that doesn't support HTTP/2 or that interferes with ALPN may negate the benefits of HTTP/2. Test your proxy's behavior with a simple HTTP/2 request before rolling it out.

Security-wise, never disable SSL verification in production. If you're dealing with a self-signed certificate, add it to a CA bundle and pass that bundle. This keeps your code secure and makes the trust boundary explicit. Also be aware that when you use a proxy, the proxy sees the hostname and the full URL path for HTTP requests, but for HTTPS requests it only sees the hostname (because of CONNECT). This is a privacy consideration if the proxy is not fully trusted.

Production Configuration for Proxy, SSL, and HTTP/2

In a production environment, you typically want to centralize client configuration so that all requests use the same proxy, CA bundle, and HTTP/2 settings. A common pattern is to create a factory function or a module-level client instance:

import httpx _client = None def get_client(): global _client if _client is None: _client = httpx.Client( proxy="http://proxy.internal:8080", verify="/etc/ssl/certs/corp-ca.pem", http2=True, timeout=30.0, ) return _client

Reusing a single client instance is important for connection pooling. Each httpx.Client maintains its own connection pool, and creating a new client per request defeats the purpose of keep-alive connections and HTTP/2 multiplexing. For async code, use httpx.AsyncClient with the same settings.

You should also set timeouts explicitly. A proxy that hangs or a server that doesn't respond can cause your application to stall indefinitely if no timeout is configured. httpx defaults to 5 seconds, but that may be too short for some operations. Adjust it based on your service-level agreements.

Finally, consider using httpx.Client as a context manager in long-running applications. It ensures the connection pool is properly closed when the client is no longer needed. For a service that lives for the entire process, you can keep the client open and close it during shutdown.

When you combine proxy, SSL, and HTTP/2, the configuration is straightforward, but the interactions are subtle. Test each feature in isolation first, then together, and always verify the actual protocol version and certificate chain in your logs. This will save you from debugging intermittent failures that only appear in production.

python httpx proxy ssl and http2: Practical Usage and Code E | RYUSLOG DEV