Documentation Python quickstart Blog Free tools Enterprise solutions hello@quanticdata.ioLog in

httpx Proxy: Setup, Auth and the proxies= Fix

httpx routes a request through a proxy, by forwarding or by tunnelling
httpx routes a request through a proxy, by forwarding or by tunnelling

To use a proxy with Python httpx, pass a proxy URL to the client as proxy=, not proxies=. The plural form was deprecated in httpx 0.26 and removed in 0.28, which is why so much working code suddenly raises TypeError: Client.__init__() got an unexpected keyword argument 'proxies'.

The httpx proxy setting in one line

httpx takes the proxy as a single URL, applied to every request the client makes:

import httpx

with httpx.Client(proxy="http://198.51.100.10:8080", timeout=10) as client:
    r = client.get("https://api.ipify.org?format=json")
    print(r.text)

The same argument exists on the top-level helpers and on the async client:

httpx.get("https://example.com/", proxy="http://198.51.100.10:8080")

async with httpx.AsyncClient(proxy="http://198.51.100.10:8080") as client:
    r = await client.get("https://example.com/")

One URL covers both schemes. You do not need a separate entry for https://, and the proxy URL itself should almost always start with http:// even when the target is HTTPS, because the connection to the proxy is plain HTTP before the tunnel is opened. If the printed IP is the proxy exit rather than your own address, the route works. The proxy tester runs the same check in the browser when you want a second opinion without writing code.

proxies= vs proxy=: what changed, and in which version

The rename is the single biggest source of httpx proxy questions, and the answer depends on the version you have installed. We installed five httpx releases in isolated virtualenvs on Python 3.13.6 with OpenSSL 3.6.3, and called each form on 10 September 2026. This is what each version does:

httpxClient(proxies=...)Client(proxy=...)mounts= with a string
0.23.3accepted, silentTypeError, unexpected keyword argument 'proxy'AttributeError, needs httpx.Proxy(...)
0.25.2accepted, silentTypeError, unexpected keyword argument 'proxy'AttributeError, needs httpx.Proxy(...)
0.26.0accepted, DeprecationWarningacceptedaccepted
0.27.2accepted, DeprecationWarningacceptedaccepted
0.28.1TypeError, unexpected keyword argument 'proxies'acceptedaccepted

The deprecation text in 0.26 and 0.27 is exact and worth grepping your logs for: The 'proxies' argument is now deprecated. Use 'proxy' or 'mounts' instead. Because it is a DeprecationWarning, Python hides it by default outside __main__, so most projects never saw it and met the removal as a hard failure two releases later.

Three practical consequences follow from that table.

  • If you own the code, replace proxies="http://..." with proxy="http://..." and require httpx>=0.26. A dict of per-scheme proxies becomes mounts, covered below.
  • If a dependency raises it, the traceback usually points at a library that still passes proxies down into httpx, and the file in the traceback is not yours. Upgrading that library is the fix; pinning httpx<0.28 only buys time.
  • A dict passed to the new argument is not a shortcut. On 0.26 and 0.27 proxy={"http://": ...} was accepted and routed anyway; on 0.28.1 the same line raises AttributeError: 'dict' object has no attribute 'url', which reads like a bug in httpx and is not one.

Authentication, and what the proxy actually receives

Credentials go in the userinfo part of the proxy URL. There is no separate auth argument:

proxy = "http://USER:PASSWORD@proxy.example.net:8080"

with httpx.Client(proxy=proxy, timeout=10) as client:
    r = client.get("https://example.com/")

Percent-encode any of @ : / # that appear in the password, or the URL parser will split the host in the wrong place. That single mistake is behind most 407 Proxy Authentication Required reports.

To see what httpx sends rather than guess, we pointed it at a socket-level stub that logs the first bytes of every connection. Same client, same credentials, two targets:

Target URLFirst line the proxy seesHeaders on that line
http://example.com/ipGET http://example.com/ip HTTP/1.1Host, Proxy-Authorization, Accept, Accept-Encoding, Connection, User-Agent
https://example.com/ipCONNECT example.com:443 HTTP/1.1Host, Accept, Proxy-Authorization only

Two things are worth taking away. First, an HTTP target is forwarded in absolute form and the proxy reads the whole request, path and headers included; an HTTPS target is tunnelled and the proxy learns only the hostname and port. Second, the forwarded request carries User-Agent: python-httpx/0.28.1 unless you override it. A default client announces the library and its exact version to every site you touch, which is a fingerprint no proxy quality can hide. Set a realistic headers= dict on the client before you blame the IP pool.

Per-domain routing with mounts, and the per-request habit that breaks

httpx has no per-request proxy. client.get(url, proxy=...) raises TypeError: Client.get() got an unexpected keyword argument 'proxy' on every version we tested, 0.23.3 through 0.28.1. This is the main behavioural gap for people arriving from requests, where the proxy is a per-call argument. In httpx the proxy is a property of the client, so routing decisions are made when you build it:

transport = httpx.HTTPTransport(proxy="http://198.51.100.10:8080")

client = httpx.Client(mounts={
    "all://internal.example.com": None,   # never proxied
    "all://": transport,                  # everything else
})

A None mount means direct. We verified the split against the stub: a request to the mounted host arrived at the proxy, a request to the excluded host went straight out and returned the real site's 404. Note that on 0.23 and 0.25 the transport wants httpx.Proxy("http://...") rather than a bare string, which is a second reason those versions are painful to support.

Rotation follows from the same rule. Because a client owns its proxy, you rotate by picking a client, not by picking a URL:

import itertools, httpx

# credentials go in each URL, exactly as shown above
exits = ["http://gate.example.net:8080?session=a",
         "http://gate.example.net:8080?session=b"]
clients = itertools.cycle([httpx.Client(proxy=p, timeout=15) for p in exits])

for url in urls:
    r = next(clients).get(url)

Building one client per request instead throws away the connection pool and, with it, most of the speed advantage of httpx. If what you actually want is a different IP per request, take it from the endpoint rather than from your code: rotating proxies give a fresh exit on every request through one host and one credential, and sticky sessions when a flow has to keep the same IP.

SOCKS5 with httpx

SOCKS support is an optional extra. Without it, the failure is explicit rather than mysterious: ImportError: Using SOCKS proxy, but the 'socksio' package is not installed. Install it and the same proxy= argument takes a SOCKS URL:

pip install "httpx[socks]"
with httpx.Client(proxy="socks5://USER:PASSWORD@proxy.example.net:1080") as client:
    r = client.get("https://example.com/")

A detail that trips people migrating from requests: there, socks5 resolves DNS locally and socks5h resolves it at the proxy, and choosing wrong leaks your resolver or breaks internal hostnames. We ran both schemes against a SOCKS5 stub that decodes the connect request. Both sent address type 3 with the literal hostname example.com and port 443. In httpx, socks5 and socks5h behave the same way, and the name is always resolved at the exit. If you need SOCKS on a commercial pool, SOCKS5 proxies expose the same credentials over both protocols, so the switch is a scheme change and nothing else.

Environment variables and how to ignore them

httpx reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY by default. We confirmed both halves against the stub: with HTTP_PROXY set, a plain httpx.Client() routed through it with no argument at all, and adding NO_PROXY=example.com sent the next request direct.

That default is convenient on a laptop and dangerous in production, where a stray variable in a container image silently reroutes traffic. Two ways to be explicit:

  • httpx.Client(trust_env=False) ignores the environment entirely.
  • httpx.Client(proxy=...) takes precedence over the variables, so an explicit proxy is never overridden by one.

When a request behaves differently inside a job runner than on your machine, print the environment before you debug the code. It is the most common cause of a "proxy not working" report that turns out to be a proxy working perfectly, for the wrong destination.

The errors you will actually hit

MessageWhat it means
TypeError: ... unexpected keyword argument 'proxies'httpx 0.28 or newer. Rename to proxy, or upgrade the library that passes it.
TypeError: ... unexpected keyword argument 'proxy'httpx 0.25 or older. Upgrade, or use proxies on that version.
AttributeError: 'dict' object has no attribute 'url'A dict was passed to proxy=. Use mounts= for per-scheme routing.
ImportError: ... 'socksio' package is not installedA SOCKS URL without the extra. Install httpx[socks].
httpx.ProxyErrorThe proxy answered and refused: bad credentials, blocked target, exhausted plan.
httpx.ConnectErrorThe proxy never answered: wrong port, wrong scheme, firewall, dead exit.
httpx.ConnectTimeout on HTTPS onlyThe proxy accepts connections but does not honour CONNECT.

The same taxonomy applies one library over: if you also maintain requests code, the exception classes differ but the causes do not, and the guide to requests.exceptions.ProxyError maps them the same way.

When the proxy layer stops being the interesting part

Everything above is plumbing. It matters right up to the point where the target starts returning challenges instead of pages, and then no amount of argument-renaming helps. At that point you are choosing between running the retry, header and fingerprint logic yourself on top of residential proxies, or handing the whole fetch to an API and paying only for the pages that come back. The web scraping API bills successful responses from $0.0002 per page and returns Markdown or structured JSON, which removes the client-side proxy question entirely; residential exits start at $0.80 per GB if you would rather keep the loop in your own code. Both sit on the same $2 of free monthly usage, which is enough to test either path before deciding.

Sources & further reading

FAQ

Quick answers on httpx proxy.

Something else? Ask us →

How do I fix "Client.__init__() got an unexpected keyword argument proxies"?

You are on httpx 0.28 or newer, where the proxies argument was removed. In your own code, rename it to proxy and pass a single URL string. If the traceback points inside another package, that package is passing proxies to httpx: upgrade it. Pinning httpx<0.28 works but leaves you on an old release.

Does httpx support a different proxy per request?

No. client.get(url, proxy=...) raises a TypeError on every version we tested, from 0.23.3 to 0.28.1. The proxy belongs to the client. Keep one client per exit and choose the client, or use mounts to route by scheme and hostname.

Should the proxy URL use http:// or https:// for an HTTPS target?

Use http://. The scheme describes how you talk to the proxy, not how the proxy talks to the site. httpx opens a plain connection, sends CONNECT, and performs the TLS handshake with the target through the tunnel, so the target is still reached over HTTPS.

What is the difference between socks5 and socks5h in httpx?

Nothing, in practice. Against a SOCKS5 stub that decodes the connect request, both schemes sent the hostname to the proxy rather than a resolved address, so DNS happens at the exit either way. Both need the httpx[socks] extra.

Why does my request go through a proxy I never configured?

httpx trusts the environment by default, so HTTP_PROXY, HTTPS_PROXY or ALL_PROXY set in a shell, image or CI runner will be used. Pass trust_env=False to ignore them, or set NO_PROXY for the hosts that must go direct.

How do I check the proxy is really being used?

Request an IP echo endpoint through the client and compare the answer with your own address. If they match, the request went direct. A response that shows the exit IP confirms the route, and is the fastest way to separate a configuration problem from a blocking problem.

Proxies that behave the same on every httpx version

One endpoint, HTTP and SOCKS5 on the same credentials, rotating or sticky exits, and a scraping API for the pages that fight back. Every account gets $2 of free usage each month.

Related reading