HTTPX proxy setup: authentication, environment and timeouts

14 min read

Set an HTTPX proxy with proxy=, pass credentials, keep HTTPS on CONNECT, and tell environment defaults apart from explicit mounts and timeouts.

This guide is for Python developers who need HTTPX 0.28.1 to leave through an HTTP or SOCKS5 proxy. Private editorial checks used loopback origins and a dummy CONNECT proxy, not a vendor account. No free keys, and Top Proxy does not sell IPs. Those checks prove that laboratory route only. They are not a public download.

01Route a request with proxy=

The installed client takes proxy= on httpx.get and on httpx.Client. Older HTTPX also accepted a proxies= mapping of URL patterns to proxy URLs. That was HTTPX itself, not a Requests-only dict. HTTPX 0.28.0 (28 November 2024) removed the deprecated proxies argument. This walkthrough stays on 0.28.1 proxy= and mounts=. The one-proxy form is a URL string or an httpx.Proxy object.

HTTP proxies (accessed 14 October 2026) says you pass that URL at client init or on the top-level helpers. A single proxy="http://127.0.0.1:PORT" sends both HTTP and HTTPS through that hop. HTTPS does not need an https:// proxy URL. The target scheme and the proxy scheme are different knobs.

  1. Pin the client you will actually import. This walkthrough used HTTPX 0.28.1, httpcore 1.0.9, and Python 3.14.6.
  2. Build a Client with proxy= set to the HTTP proxy URL. Leave trust_env=False while you prove the explicit path, so leftover shell variables cannot steal the route.
  3. Fetch an HTTP origin you control. Read the proxy log, not only the status code. The lab's dummy forwarder recorded GET http://httpx-lab.origin:<port>/ with authorized: true.
  4. Close the client with a context manager. That closes the transport pool. A forgotten client is a leaked socket, not a faster script.

The direct baseline matters. After clearing HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY in a child process, a client with no proxy= hit the HTTP origin and the dummy proxy stayed idle. That is the no-proxy control. It does not tell you what HTTPX would do if those variables were still set. That comes later.

If you need two exits, do not stuff two URLs into proxy=. Use mounts with HTTPTransport(proxy=...) per pattern, and put verify, trust_env, and retries on each transport. The client-level verify= argument did not ride along in this build. A mount that only inherited Client(verify=ctx) failed TLS with ConnectError / SSLCertVerificationError: unable to get local issuer certificate. The same request succeeded when the transport itself received verify=ctx.

02Choose an HTTP proxy for an HTTPS target

People name this an "HTTPS proxy" because the website is https://. The hop in front is still an HTTP proxy that speaks CONNECT. The docs call that out as a gotcha: the https:// key in a mounts dict is the target scheme, and the proxy URL in front of it is usually http://.

Forwarding and tunnelling are not the same mechanism. For http:// the proxy makes the origin request and returns the origin bytes. For https:// the client sends CONNECT host:443, the proxy opens TCP, and TLS runs between client and origin through that tunnel. A fake 200 from CONNECT without a working byte tunnel is not HTTPS proxying. The lab forwarded real origin bodies on HTTP and real TLS bytes after CONNECT.

  1. Keep the proxy URL on http://127.0.0.1:<port> even when the target is https://httpx-lab.origin:<port>/.
  2. Trust the origin with ssl.create_default_context(cafile=ca.pem) and pass that context as verify=. Do not turn verification off.
  3. Confirm the proxy log shows CONNECT httpx-lab.origin:<https-port> before you celebrate a 200.
  4. Repeat once with the default CA store and no lab CA. You want a TLS failure that still logged CONNECT.

That last step is the one operators skip. In this run, CONNECT succeeded, then HTTPX raised ConnectError wrapping SSLCertVerificationError: unable to get local issuer certificate. The tunnel was fine. The certificate was not. A browser What is my IP page would have shown the browser's exit, not this client. Use the proxy checker to see whether a host answers HTTP or SOCKS5 before you point HTTPX at it.

SOCKS5 is a third path. pip install httpx[socks] pulled socksio 1.0.0. httpx.Proxy in 0.28.1 accepts socks5 and socks5h. Against a controlled SOCKS5 fixture, socks5://127.0.0.1:<port> to http://127.0.0.1:<origin>/ sent ATYP ipv4. The same client with socks5h:// and a hostname target sent ATYP domain httpx-lab.origin. Using socks5:// with that hostname also sent ATYP domain. This build did not locally resolve the hostname and then send an IPv4 address the way some curl socks5 setups do. Do not copy socks5h folklore from Requests into HTTPX. Measure the handshake.

03Set proxy authentication

Proxy credentials are not auth=. auth= is origin authentication. Mix them and you will debug the wrong hop for an hour.

The proxies page puts userinfo in the proxy URL. The API also documents httpx.Proxy(url, auth=(user, password)). Both worked in the lab. The dummy password included @, :, /, #, and %. Those characters belong in the userinfo only after component-wise URL encoding, or in the auth= tuple on Proxy, which never goes through URL parsing. Logs and figures redacted the value. Do not print Proxy-Authorization when you debug.

Missing and wrong credentials are not one behavior.

On HTTP forwarding, a request with no proxy user returned HTTP 407 from the fixture. HTTPX surfaced that as status_code == 407. A wrong password did the same. The fixture logged authReason: missing and authReason: wrong. No exception. If your wrapper later throws, keep the 407.

On HTTPS CONNECT, the same missing or wrong dummy credentials raised httpx.ProxyError with message 407 Proxy Authentication Required. There was no response object to read. CONNECT is a tunnel setup. A 407 there is a transport failure, not an origin page.

Correct credentials returned origin 200 on both paths. The origin never saw Proxy-Authorization. That header stays on the proxy hop. If you need origin Basic as well, send auth=(origin_user, origin_pass) on the client. The lab's /origin-auth route returned 401 without it and 200 with both hops authenticated.

  1. Put proxy user and password in httpx.Proxy(..., auth=(user, password)) when the password has reserved characters.
  2. Keep origin auth= for the website, not the proxy.
  3. Treat HTTP 407 and CONNECT ProxyError as different outcomes. Record the class you actually caught.
  4. Reject the idea that "it works in curl" proves HTTPX sent the same header. Prove it on this client.

Residential products document the same split. Oxylabs Residential Proxies (accessed 14 October 2026) lists HTTP, HTTPS, HTTP/3, and SOCKS5, with username/password or IP allowlisting, and entry pr.oxylabs.io:7777 using customer-USERNAME:PASSWORD. That is a documented endpoint shape, not a test of their pool. Decodo residential quick start (same day) shows gate.decodo.com:7000 and a Python mapping that sends both http and https through one HTTP proxy URL. The English catalog still lists that product as smartproxy-com. The public name on the docs is Decodo. The path did not change.

04Understand environment defaults and mounts

HTTPX reads the environment by default. Environment variables (accessed 14 October 2026) lists HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY, plus SSL_CERT_FILE and SSL_CERT_DIR. trust_env=False turns that whole set off. It does not mean "skip certificate checks".

The child lab process started with those variables removed, plus CGI leftovers, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, and SSLKEYLOGFILE. It used an empty private NETRC file. Workstation HOME was left alone. Each environment experiment started from that baseline and injected only named dummy values.

Loopback bypass will lie to you. HTTP clients often skip 127.0.0.1. The env-proxy cases used hostname httpx-lab.origin and NO_PROXY="", with a scoped getaddrinfo patch that maps that name to loopback. That patch is injected resolver behavior. It is not a measurement of public DNS.

  1. Export HTTP_PROXY and HTTPS_PROXY to the encoded proxy URL. Set NO_PROXY to an empty string when the target must use the proxy.
  2. Construct Client(trust_env=True) with no proxy= argument. The lab request returned 200 and only proxy alpha logged the GET. Proxy bravo stayed idle.
  3. Repeat with NO_PROXY=httpx-lab.origin. The origin still returned 200. Alpha did not see that GET. That is bypass, and it is a dedicated case, not a side effect you assume.
  4. Repeat with the same env vars and trust_env=False. The request went direct. Alpha saw nothing.

Explicit proxy= won over a conflicting environment. With alpha in HTTP_PROXY and proxy= pointing at bravo, bravo got the GET and alpha did not. Status 200 alone would not have told you which hop was used. Two instrumented identities did.

trust_env=False also stopped SSL_CERT_FILE from becoming the client CA store. With the lab CA in SSL_CERT_FILE and trust_env=True, HTTPS to the lab origin returned 200. With trust_env=False and default verify=True, the same URL raised ConnectError / SSLCertVerificationError. Passing verify=ssl.create_default_context(cafile=ca.pem) on that trust_env=False client restored 200. Verification stayed on. Only the env lookup went away.

transport= and mounts= are different constructor arguments. HTTPX 0.28.1 sets allow_env_proxies = trust_env and transport is None. A custom transport= object skips environment proxy mounts. A mounts= dict does not. The client builds env mounts first, then updates that map with your mounts. More specific URL patterns win. http:// from HTTP_PROXY beats all://, so Client(mounts={"all://": some_transport}, trust_env=True) can still send HTTP targets through the inherited proxy. Set trust_env=False when environment routing must be excluded. Put proxy= and verify= on each mounted HTTPTransport. Client-level verify= still does not copy onto those transports. That last miss is how the untrusted-CA failure sneaks back in.

05Test sync and async lifecycle

Sync and async are two clients. They share the proxy= shape. They do not share a connection pool.

with httpx.Client(proxy=..., verify=ctx) as client: is the sync form. Close happens on context exit. async with httpx.AsyncClient(proxy=..., verify=ctx) as client: is the async form. You await client.get(...). Mixing get without await inside the async client is a bug, not a timeout.

The async case in this lab used AsyncClient with the same HTTP proxy URL and the lab SSL context. await client.get(https_url) returned 200 with the HTTPS origin mark. The proxy logged CONNECT. That is one successful async tunnel. It is not a concurrency benchmark, and it says nothing about pool reuse under load.

Mounted transports need the same close path. HTTPTransport created by hand is closed by the client if the client owns it through mounts. If you keep a transport variable alive across clients, call transport.close() in finally. The lab did that on the retry and mount cases.

Retries belong on the transport. Transports (accessed 14 October 2026) says HTTPTransport(retries=...) retries ConnectError and ConnectTimeout. It does not retry 429 or 503. The lab set retries=2 and fetched /status/503. The origin counted one hit. HTTPX returned 503. That is not urllib3 Retry. Do not paste allowed_methods folklore into HTTPX.

06Diagnose connection, body-read and TLS failures

Name the exception you caught. HTTPX has a real hierarchy. Collapsing everything to "proxy error" hides the hop that failed.

A closed loopback listener produced ConnectError wrapping ConnectionRefusedError: [Errno 61] Connection refused. That is a refusal. It is not a connect timeout. On this Darwin kernel, a socket that was bound and held but never put into listen did not refuse. HTTPX raised ConnectTimeout wrapping TimeoutError after the configured connect deadline. That wait is labeled injected-fixture behavior for bind-without-listen. It is not a measurement of the public internet.

Read timeouts are about chunks after the response has started. The origin /slow-body sent complete headers immediately, wrote and flushed one body byte, then waited longer than Timeout(read=0.45). Streaming the body, HTTPX returned status 200, the first-byte event fired, one byte arrived, then ReadTimeout wrapping TimeoutError. Headers had already landed. ConnectError would have been the wrong class. ConnectTimeout would have been the wrong class. If you only call response.text without streaming, you still time out, but you lose the proof that headers arrived.

TLS failures after CONNECT stayed ConnectError with SSLCertVerificationError. The proxy had already accepted the tunnel. Turning verify=False would hide that. Do not.

Use mobile proxies or residential proxies when the job is an ISP-assigned exit. Protocol support and IP origin are different dimensions. This lab never claimed a country, a success rate, or an account.

FailureWhat the 0.28.1 lab raisedWhat it is not
HTTP proxy, missing or wrong BasicResponse status 407A CONNECT exception
HTTPS CONNECT, missing or wrong BasicProxyError ("407 Proxy Authentication Required")An origin 407 page
Lab HTTPS without the lab CAConnectError / SSLCertVerificationError after CONNECTProof that CONNECT never happened
Closed loopback portConnectError / ConnectionRefusedErrorConnectTimeout
Bind without listen on DarwinConnectTimeout (fixture-specific wait)A refusal, or a WAN RTT
Delayed body after first byteReadTimeout while iterating iter_bytes()A connect failure
Origin /status/503 with retries=2Status 503, one origin hitAn application retry
Assertion table from the HTTPX 0.28.1 loopback lab on 14 October 2026

The figure is the assertion table from this run. It is not a cover image and not a screenshot of a checker UI.

The next block is a schematic API sketch. The ports, CA path, and password are placeholders you replace with values you control. It is not a public fixture bundle and it will not run from an empty directory.

import httpx
import ssl

# PLACEHOLDERS. Do not treat these literals as lab values.
ctx = ssl.create_default_context(cafile="PATH/TO/LAB_CA.pem")
proxy = httpx.Proxy(
    "http://127.0.0.1:PROXY_PORT",
    auth=("PROXY_USER", "PROXY_PASSWORD"),
)
timeout = httpx.Timeout(5.0, connect=2.0, read=0.45)

with httpx.Client(proxy=proxy, verify=ctx, trust_env=False, timeout=timeout) as client:
    http_response = client.get("http://TARGET_HOST:HTTP_PORT/")
    https_response = client.get("https://TARGET_HOST:HTTPS_PORT/")

The 0.28.1 exception classes and CONNECT versus 407 outcomes in this article come from a private editorial fixture. That fixture is not published here. Install a client you can import with python -m pip install "httpx[socks]==0.28.1", which depends on httpcore 1.* and, for SOCKS, socksio. Put your HTTP proxy host, port, and credentials into the placeholders, using httpx.Proxy(..., auth=(user, password)) when the password has reserved characters. Point the HTTP URL at an origin you control for forwarding, and the HTTPS URL at an origin you control for CONNECT then TLS. For HTTPS, pass verify= as ssl.create_default_context(cafile=...) with a CA file you already trust for that origin. Keep trust_env=False while you prove the explicit proxy= path. Do not set verify=False.

07Choose a documented endpoint

Once the client path is honest, pick an entry host that documents HTTP forwarding and CONNECT. The English catalog pages for Oxylabs and Decodo (still listed as smartproxy-com) returned live self-canonical URLs on 14 October 2026. Their official docs describe HTTP gateways with username/password or allowlisted IP. That is endpoint fit on paper. This article did not log into either product and does not rank them.

Oxylabs — residential and datacenter proxies
Oxylabs — residential and datacenter proxies4.0770

Oxylabs sells residential, datacenter and mobile proxies. Residential self-serve starts at $6/GB for 5 GB/month. Buyer payments: cards, wire, AliPay, PayPal.

10 countries
$0.59
per 1 GB
Decodo (Smartproxy) — residential and datacenter proxies
Decodo (Smartproxy) — residential and datacenter proxies4.22106

Decodo (formerly Smartproxy) sells residential and datacenter proxies. Residential: $4/GB without a monthly plan, or monthly packs from $3.75/GB for 3 GB. Payments: cards, PayPal, Alipay, Google/Apple Pay, crypto (with limits).

83 countries
$0.38
per 1 GB

If you only needed a browser confirmation, stay on What is my IP. If you needed HTTPX, believe the proxy log and the exception class from this library. The two disagree more often than the blog posts admit.

How do I set an HTTPX proxy?

Pass proxy= to httpx.Client or httpx.get. Use a URL string or httpx.Proxy. HTTPX 0.28.1 has no proxies= argument. HTTPX removed that deprecated mapping in 0.28.0. For per-scheme routing, mount HTTPTransport objects that each receive their own proxy and verify arguments. mounts= still merges environment patterns when trust_env=True. Use transport= or trust_env=False when inherited HTTP_PROXY must not apply.

Why use http:// for a proxy handling HTTPS?

The proxy hop is usually plain HTTP. HTTPS targets use CONNECT through that hop, then TLS to the origin. The https:// key in mounts is the target scheme. The proxy URL in front of it stays http:// unless the product actually speaks TLS to the proxy itself.

How do proxy credentials differ from auth=?

Proxy userinfo or httpx.Proxy(auth=(user, password)) send Proxy-Authorization to the proxy. auth= is origin authentication. The lab returned proxy 407 or ProxyError when proxy credentials were wrong, and origin 401 when only origin credentials were missing.

Does trust_env=False disable certificate verification?

No. trust_env=False stops HTTPX from reading HTTP_PROXY, NO_PROXY, SSL_CERT_FILE, and SSL_CERT_DIR. TLS verification stays on. HTTPX does not load origin credentials from netrc because trust_env is true. If you pass httpx.NetRCAuth yourself, that auth still applies with trust_env=False. Pass ssl.create_default_context(cafile=...) as verify= when you need a private CA. verify=False is not the switch for environment handling.

Why does the client raise ReadTimeout after headers arrive?

The read timeout is a per-chunk body deadline. The lab origin sent headers and one body byte, then paused. Streaming returned status 200, delivered that byte, then raised ReadTimeout. That is not ConnectTimeout and not a failure to speak to the proxy.

Does HTTPTransport retry 429 or 503?

No. retries on HTTPTransport retries ConnectError and ConnectTimeout only. With retries=2 the lab origin /status/503 was hit once and HTTPX returned 503. Use another library if you need status retries.