๐ŸŽ‰ Premium Proxies ยท 24-Hour Free TrialClaim Now
Guides

httpx proxies Argument Removed? Fix ProxyError and 407

The httpx proxies argument is gone in 0.28. Fix the TypeError, then ProxyError 407, SOCKS ImportError and RemoteProtocolError, with verbatim errors and code.

S SparkProxy 2 4 min read
Share

The httpx proxies argument no longer exists, so a scraper that ran fine last year now dies before sending a single request. Fix that and the next layer shows up: a ProxyError with a 407, an ImportError about socksio, or a RemoteProtocolError that only appears with HTTP/2 switched on. This guide starts from the exact text in your terminal, checks the cheapest thing first, and gives a before/after fix for each cause.

Every error string below was reproduced on 2026-10-30 on Windows with Python 3.14.2, using httpx 0.28.1 and 0.27.2 (both on httpcore 1.0.9), against SparkProxy's live gateway from a machine that is deliberately not whitelisted, plus two small local proxies written to simulate dead tunnels.

Match your error to a cause

Find the line that matches your traceback, then jump to the section. The left column is pasted from our runs, not paraphrased.

What you seeMost likely causeSection
`TypeError: Client.__init__() got an unexpected keyword argument 'proxies'`httpx 0.28 or later; `proxies` was removed[TypeError](#typeerror-proxies)
`TypeError: get() got an unexpected keyword argument 'proxies'`Same removal, top-level helper[TypeError](#typeerror-proxies)
`httpx.ProxyError: 407 Proxy Authentication Required`No credentials, wrong credentials, or IP not whitelisted[407](#proxy-407)
`httpx.InvalidURL: Invalid port: 'abc'`A `#` or `/` in the proxy password[Bad URL](#bad-proxy-url)
`httpx.ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version number``https://` scheme on a plain HTTP proxy port[Bad URL](#bad-proxy-url)
`ImportError: Using SOCKS proxy, but the 'socksio' package is not installed.``httpx[socks]` extra missing[SOCKS](#socks-extra)
`ValueError: Unknown scheme for proxy URL URL('socks5h://...')``socks5h` on httpx older than 0.28, or no scheme at all[SOCKS](#socks-extra)
`httpx.RemoteProtocolError: Server disconnected without sending a response.`HTTP proxy URL aimed at a SOCKS port, or a dead pooled tunnel[RemoteProtocolError](#remote-protocol-error)
`httpx.RemoteProtocolError: Server disconnected`Same, on an HTTP/2 connection[RemoteProtocolError](#remote-protocol-error)

Only the 407 row and some of the disconnect rows involve the proxy service at all. Everything else is your code or your install.

Check the installed httpx version first

This takes five seconds and decides which half of the advice applies:

python -c "import httpx, httpcore; print(httpx.__version__, httpcore.__version__)"
pip show httpx | grep -i '^version'

The proxy API changed across three releases. Dates below are the upload times on PyPI's JSON API and the entries in the project's own CHANGELOG.md, both read today.

httpx releasePyPI upload dateWhat changed for proxies
0.25.22023-11-24Last release where `proxies=` is the only spelling
0.26.02023-12-20`proxy=` added; `proxies=` deprecated but still works
0.27.02024-02-21`app=` deprecated; `http1` now respected on proxy transports
0.28.02024-11-28`proxies=` and `app=` removed; `socks5h` accepted as a scheme
0.28.12024-12-06SSL fix only; current stable release

Why did so many people miss a year of deprecation? Because the warning is a DeprecationWarning raised inside the httpx module, and Python hides those by default unless they're triggered from __main__. We ran httpx.Client(proxies=...) on 0.27.2 with no flags and got no output at all. Only python -W always printed it:

DeprecationWarning: The 'proxies' argument is now deprecated. Use 'proxy' or 'mounts' instead.

If you want the next removal to announce itself, run your test suite with -W error::DeprecationWarning.

One more thing in the version check. PyPI also lists 1.0.dev1 through 1.0.dev6 (the latest uploaded 2026-08-31). Those are pre-releases, so pip install httpx won't pick them, but an unpinned --pre install will. As for activity: the default branch's newest commit is dated 2026-02-23 and the repository was last pushed on 2026-10-02 per the GitHub API, so the project is alive but stable releases are infrequent. Pinning httpx>=0.28,<0.29 in production is reasonable.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

TypeError: unexpected keyword argument 'proxies'

On 0.28.1, every entry point fails the same way. These four lines are verbatim:

TypeError: Client.__init__() got an unexpected keyword argument 'proxies'
TypeError: Client.__init__() got an unexpected keyword argument 'proxies'
TypeError: get() got an unexpected keyword argument 'proxies'
TypeError: AsyncClient.__init__() got an unexpected keyword argument 'proxies'

The first came from a dict (proxies={"all://": ...}), the second from a bare string. If you used one proxy for everything, the fix is a rename. Before:

import httpx

# httpx 0.25 style: raises TypeError on 0.28+
client = httpx.Client(proxies="http://sp_user:sp_pass@gateway.sparkproxy.io:11000")
r = httpx.get("https://www.sparkproxy.io/", proxies={"all://": "http://gateway.sparkproxy.io:11000"})

After:

import httpx

PROXY = "http://sp_user:sp_pass@gateway.sparkproxy.io:11000"

with httpx.Client(proxy=PROXY, timeout=20) as client:
    r = client.get("https://www.sparkproxy.io/")

r = httpx.get("https://www.sparkproxy.io/", proxy=PROXY, timeout=20)

AsyncClient takes the same proxy= keyword. The broader library differences are covered in our requests vs httpx comparison, so we won't repeat them here.

When the traceback points into site-packages rather than your own file, a dependency is still passing proxies= for you. You have two honest options: upgrade that dependency to a release that speaks the new API, or pin httpx<0.28 until it does. Monkey-patching httpx to accept the old keyword hides the problem until the next upgrade.

Translating a per-scheme proxies dict into mounts

The old dict let you send http:// and https:// traffic to different proxies. That job now belongs to mounts, which maps a URL pattern to a transport. Before:

import httpx

client = httpx.Client(proxies={
    "http://": "http://sp_user:sp_pass@gateway.sparkproxy.io:11000",
    "https://": "http://sp_user:sp_pass@gateway.sparkproxy.io:11002",
})

After, with two explicit transports and two hosts that skip the proxy:

import httpx

ROTATING = "http://sp_user:sp_pass@gateway.sparkproxy.io:11000"
STICKY = "http://sp_user:sp_pass@gateway.sparkproxy.io:11002"

mounts = {
    "http://": httpx.HTTPTransport(proxy=ROTATING),
    "https://": httpx.HTTPTransport(proxy=STICKY, http2=True, retries=2),
    "all://www.sparkproxy.io": None,      # exact host goes direct
    "all://*.sparkproxy.io": None,    # and every subdomain
}

with httpx.Client(mounts=mounts, timeout=20) as client:
    r = client.get("https://www.sparkproxy.io/robots.txt")

Note the http:// scheme on the HTTPS proxy URL. The httpx docs flag this as a common gotcha: the proxy URL describes how you talk to the proxy, and most proxies (SparkProxy's gateway included) take plain HTTP and open a CONNECT tunnel for HTTPS targets.

Three things about mounts tripped us up while testing, and none of them raise a helpful error.

Client options don't reach mounted transports. httpx.Client(http2=True, mounts={"all://": httpx.HTTPTransport(proxy=P)}) returned HTTP/1.1 in our run. The same client built with proxy=P returned HTTP/2. httpx builds transports for proxy= itself and copies your http2, verify, limits and trust_env settings into them; a transport you construct is used exactly as you built it. Pass those options to the transport, as the example above does with http2=True.

Async clients need async transports. Putting a sync HTTPTransport inside AsyncClient(mounts=...) failed on entry to async with with this (the "Did you mean" suffix comes from Python 3.14's error hints):

AttributeError: 'HTTPTransport' object has no attribute '__aenter__'. Did you mean: '__enter__'?

Use httpx.AsyncHTTPTransport(proxy=...) instead.

proxy= switches off environment proxies entirely, including NO_PROXY. We set HTTPS_PROXY and NO_PROXY=sparkproxy.io, then built httpx.Client(proxy=P): requests to sparkproxy.io still went through P. The source explains why: when proxy is set, httpx returns {"all://": proxy} and never reads the environment. If you need exclusions alongside an explicit proxy, write them as None mounts. Passing transport= or trust_env=False also disables environment proxies.

ProxyError: 407 Proxy Authentication Required

A 407 means you reached the proxy and it declined to forward you. That's progress: DNS, the port and the scheme are all fine. The full chain from our run against an HTTPS target:

httpx.ProxyError: 407 Proxy Authentication Required
  caused by httpcore.ProxyError: 407 Proxy Authentication Required

Here's a difference most guides skip. For a plain http:// target, httpx forwards the request instead of tunnelling it, so the proxy's 407 comes back as an ordinary response. No exception. Our run printed status 407 with a x-squid-error: ERR_CACHE_ACCESS_DENIED 0 header and a 73 KB HTML error page in r.text. If your code parses whatever comes back, you may be scraping the proxy's error page. Call r.raise_for_status() or check for 407 explicitly.

SparkProxy accepts two authentication methods (IP whitelist or user:pass), so a 407 has three possible causes:

  1. No credentials and your IP isn't on the whitelist. Cloud servers, CI runners and home connections change IPs. Either add the current egress IP in the dashboard or add credentials.
  2. Wrong credentials. We got the identical 407 text for missing and wrong credentials, so the message can't tell you which. Copy them again from the dashboard.
  3. Credentials that never reached the proxy because the URL didn't parse the way you expected. That's the next section.

httpx reads credentials from the URL's userinfo. When the password holds characters that are special in URLs, pass them separately with httpx.Proxy, which takes an auth tuple and needs no escaping:

import httpx
from urllib.parse import quote

# Option 1: keep the URL clean and pass credentials as a tuple
proxy = httpx.Proxy("http://gateway.sparkproxy.io:11000", auth=("sp_user", "abc#123"))

# Option 2: percent-encode the password inside the URL
encoded = f"http://sp_user:{quote('abc#123', safe='')}@gateway.sparkproxy.io:11000"

with httpx.Client(proxy=proxy, timeout=20) as client:
    r = client.get("https://www.sparkproxy.io/robots.txt")

Both parsed to ('sp_user', 'abc#123') in our test. An unencoded @ in the password actually works (httpx 0.26.0 added that), but a # or / does not.

For the broader meaning of 407 versus 403 and 502 at the proxy layer, see our proxy error codes reference.

ConnectError, InvalidURL and ValueError: the proxy URL is wrong

These all fail before the proxy has a chance to answer. Run them in this order, cheapest first.

Bad proxy URL (our test)Exact errorFix
`gateway.sparkproxy.io:11000` (no scheme)`ValueError: Unknown scheme for proxy URL URL('gateway.sparkproxy.io:11000')`Prefix `http://`
`http://sp_user:abc#123@gateway...``httpx.InvalidURL: Invalid port: 'abc'`Use `httpx.Proxy(auth=...)` or encode
`https://gateway.sparkproxy.io:11000``httpx.ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1081)`Use `http://` for the proxy itself
`http://gateway.sparkproxy.io:11009``httpx.ConnectError: [WinError 10061] No connection could be made because the target machine actively refused it`Wrong port; use 11000, 11002 or 13000
`http://gate.sparkproxy.io:11000` (typo)`httpx.ConnectError: [Errno 11001] getaddrinfo failed`Fix the hostname

The InvalidURL one is sneaky. The # starts a URL fragment, so the parser reads abc as the port of a host called sp_user. The error mentions a port you never typed.

WRONG_VERSION_NUMBER means httpx tried a TLS handshake with something that answered in plain text. Unless your provider explicitly sells an HTTPS proxy endpoint, the proxy URL scheme is http:// even when every target is HTTPS. On Linux the refused and DNS cases read [Errno 111] Connection refused and [Errno -2] Name or service not known; we didn't reproduce those wordings on Linux, so treat them as the standard glibc strings rather than captured output.

SOCKS proxies need httpx[socks]

httpx speaks SOCKS5 through the optional socksio package. Without it, building the client fails immediately, before any network traffic:

ImportError: Using SOCKS proxy, but the 'socksio' package is not installed. Make sure to install httpx using `pip install httpx[socks]`.

The fix is the extra, quoted so your shell doesn't treat the brackets as a glob:

pip install "httpx[socks]==0.28.1"

That pulled in socksio 1.0.0, which PyPI shows was uploaded on 2020-04-17. There has been no socksio release since.

Then the client code is the same proxy= keyword with a SOCKS URL:

import httpx

with httpx.Client(proxy="socks5://sp_user:sp_pass@gateway.sparkproxy.io:13000", timeout=20) as client:
    r = client.get("https://www.sparkproxy.io/robots.txt")
    print(r.status_code)

Two version-dependent surprises. On httpx 0.27.2, socks5h:// raised ValueError: Unknown scheme for proxy URL URL('socks5h://gateway.sparkproxy.io:13000'); 0.28.0 is the release that accepts it. And the h doesn't change anything in httpx. In requests, socks5h means "resolve DNS at the proxy". In httpcore 1.0.9, the SOCKS code passes the hostname straight to socksio, which encodes any non-IP host as a domain-name address, so DNS is resolved by the proxy with either scheme. Plain socks5:// works on every version that supports SOCKS, so use it when you can't control which httpx is installed.

From our non-whitelisted machine, the SOCKS gateway refused the connection with httpx.ProxyError: Proxy Server could not connect: Connection not allowed by ruleset. That's the SOCKS reply code for a rule rejection, and in our run it played the same role as the 407 on the HTTP port: fix the whitelist or the credentials. For SOCKS setup in other languages, our SOCKS5 setup guide has the details.

RemoteProtocolError: Server disconnected

httpcore raises two different strings for the same event. HTTP/1.1 connections say Server disconnected without sending a response. and HTTP/2 connections say Server disconnected. Both mean the TCP connection closed while httpx was waiting for a response. We found two distinct causes.

Cause 1: an HTTP proxy URL pointed at a SOCKS port. proxy="http://gateway.sparkproxy.io:13000" produced httpx.RemoteProtocolError: Server disconnected without sending a response. on every attempt. The SOCKS listener received an HTTP CONNECT line it couldn't parse and hung up. Fix the scheme or port: socks5:// for 13000, http:// for 11000 and 11002. This one fails 100% of the time, which is the clue.

Cause 2: a pooled tunnel that died while idle. This one is intermittent, and it's where HTTP/2 makes life harder. To test it without blaming a real network, we wrote a short local CONNECT proxy that treats any tunnel idle for more than 3 seconds as dead and hangs up on the next write. Then we made a request, slept 4 seconds, and made another on the same client:

HTTP/1.1 run: httpx.RemoteProtocolError: Server disconnected without sending a response.
HTTP/2 run:   httpx.RemoteProtocolError: Server disconnected

With a second variant, which closes idle tunnels on a timer instead of waiting for the next write, we ran 40 reuses timed to land right on the cutoff. HTTP/1.1 recovered all 40 times. HTTP/2 failed 6 of 40 (on Windows the failures surfaced as ReadError: [WinError 10053]). That's one synthetic test on one machine, not a rate you should expect in production, but the direction matches what the httpcore tracker describes: open pull request #1080 notes that httpcore never sends HTTP/2 PING frames, so intermediaries with idle timers kill the connection, and open PR #1054 covers races after a server sends GOAWAY.

What fixes it:

import httpx

PROXY = "http://sp_user:sp_pass@gateway.sparkproxy.io:11000"

# 1. Simplest: stay on HTTP/1.1 through proxies (the default)
client = httpx.Client(proxy=PROXY, http2=False, timeout=20)

# 2. Expire idle connections before a proxy or NAT timer does
#    (keepalive_expiry=2.0 cleared our 3-second dead-tunnel test on HTTP/1.1 and HTTP/2)
limits = httpx.Limits(max_keepalive_connections=20, keepalive_expiry=2.0)
client = httpx.Client(proxy=PROXY, limits=limits, timeout=20)


# 3. Retry the request yourself; transport retries won't cover this
def get_with_retry(client: httpx.Client, url: str, attempts: int = 3) -> httpx.Response:
    for i in range(attempts):
        try:
            return client.get(url)
        except (httpx.RemoteProtocolError, httpx.ReadError):
            if i == attempts - 1:
                raise
    raise RuntimeError("unreachable")

Fix 3 matters because httpx.HTTPTransport(retries=3) only retries ConnectError and ConnectTimeout (see httpcore/_sync/connection.py). We confirmed it: with retries=3 the dead-tunnel request still raised Server disconnected, and an immediate second call on the same client returned HTTP/2 200. Retrying a GET is safe; retrying a POST that the server may already have processed is your call. Backoff and retry budgets are covered in our proxy timeout and retry guide.

HTTP/2 also changes what rotation means. Through a CONNECT tunnel, the exit IP is fixed for the tunnel's lifetime, because the proxy relays one TCP stream it can't re-route mid-flight. HTTP/2 multiplexes every request to a host over one connection, so with http2=True a rotating gateway sees one tunnel and one exit IP for the whole batch. HTTP/1.1 keep-alive does the same thing on a smaller scale. If you want a new IP per request on port 11000, open new connections (lower keepalive_expiry, or a fresh client per batch). If you want a stable IP, that's what SparkProxy's Sticky Sessions on port 11002 are for; our sticky session explainer covers when that helps. For whether HTTP/2 is worth it for scraping at all, see HTTP/2 vs HTTP/1.1 for web scraping.

Classify any httpx proxy failure in one function

Save this as a file, pass one or more proxy URLs on the command line, and it reports which layer broke. The exception order matters: httpx.InvalidURL isn't a ValueError subclass, and ProxyError and ConnectError are siblings under TransportError, so neither catches the other.

import sys
import httpx

TARGET = "https://www.sparkproxy.io/robots.txt"


def diagnose(proxy: str, target: str = TARGET) -> str:
    try:
        with httpx.Client(proxy=proxy, timeout=15) as client:
            r = client.get(target)
    except TypeError as e:
        return f"API mismatch for httpx {httpx.__version__}: {e}"
    except ImportError as e:
        return f"missing optional dependency: {e}"
    except (ValueError, httpx.InvalidURL) as e:
        return f"proxy URL does not parse: {e}"
    except httpx.ProxyError as e:
        return f"proxy answered but refused the tunnel: {e}"
    except httpx.ConnectError as e:
        return f"never reached a working proxy (DNS, port or TLS): {e}"
    except httpx.RemoteProtocolError as e:
        return f"connection dropped mid-exchange: {e}"
    except httpx.TimeoutException as e:
        return f"{type(e).__name__}: {e}"
    if r.status_code == 407:
        return "407 returned as a normal response (plain-HTTP target)"
    return f"OK {r.status_code} over {r.http_version}"


if __name__ == "__main__":
    for p in sys.argv[1:]:
        print(f"{p}\n  -> {diagnose(p)}")

Output from our run, unedited:

http://gateway.sparkproxy.io:11000
  -> proxy answered but refused the tunnel: 407 Proxy Authentication Required
http://sp_user:abc#123@gateway.sparkproxy.io:11000
  -> proxy URL does not parse: Invalid port: 'abc'
http://gateway.sparkproxy.io:13000
  -> connection dropped mid-exchange: Server disconnected without sending a response.
https://gateway.sparkproxy.io:11000
  -> never reached a working proxy (DNS, port or TLS): [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1081)
socks5://gateway.sparkproxy.io:13000
  -> proxy answered but refused the tunnel: Proxy Server could not connect: Connection not allowed by ruleset.

Which of these errors a new proxy provider would solve

Swapping providers is the wrong move for most of the errors in this post. Be honest about which bucket yours is in.

ErrorChanging the proxy helps?Why
`TypeError ... 'proxies'`NoCode written for an older httpx
`ImportError ... socksio`NoMissing `httpx[socks]` extra
`ValueError` / `InvalidURL`NoMalformed proxy URL
`ConnectError` `WRONG_VERSION_NUMBER`NoWrong scheme for the proxy
`ConnectError` refused / getaddrinfoOnly if the endpoint is really downUsually a wrong port or host
`ProxyError: 407`No, fix authWhitelist or credentials
`RemoteProtocolError`, every attemptNoScheme/port mismatch
`RemoteProtocolError`, intermittentSometimesIdle timers and HTTP/2 reuse; fix the client first
403 / CAPTCHA from the target after all this worksYesThat's the target blocking the IP or fingerprint

The last row is the only one where IP reputation is the problem. Once httpx is talking to the proxy correctly, SparkProxy's plans give you 1M+ premium IPs across 80+ countries over HTTP/HTTPS/SOCKS5, with both IP whitelist and user:pass auth. Plans are packs priced by plan and concurrency with unlimited bandwidth included, "Starting from $75/month" (USD, as listed on sparkproxy.io on 2026-10-09), and there's a 24-hour free trial with no credit card.

If the target itself blocks you after the plumbing is fixed, our async web scraping guide covers concurrency limits and backoff with httpx.AsyncClient, which is the next thing to tune.

Bypassing proxy setup with the Scraping API

If you'd rather not manage proxy URLs, schemes and tunnels at all, the SparkProxy Scraping API is a normal HTTPS endpoint, so plain httpx with no proxy settings works. Two parameters matter for cost: render_js defaults to true (5 credits), and render_js=false is a plain fetch at 1 credit. json_response=true returns an envelope whose body is base64, so decode it before matching anything.

import base64
import os

import httpx

params = {
    "url": "https://www.sparkproxy.io/robots.txt",
    "render_js": "false",       # plain fetch, 1 credit
    "json_response": "true",    # envelope: job_id, status_code, duration_ms, credits_used, body
}
r = httpx.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": os.environ["SPARKPROXY_API_KEY"]},
    params=params,
    timeout=60,
)
r.raise_for_status()
data = r.json()
html = base64.b64decode(data["body"]).decode("utf-8", errors="replace")
print(data["status_code"], data["credits_used"], html[:120])

Don't confuse json_response with format=json: format only converts the page content (html, md, mdx, json, screenshot, pdf), it doesn't give you the envelope. New accounts get 1,000 free credits. We built this call from the API docs and ran it on 2026-10-30, but the endpoint answered with a Cloudflare 521 at that moment, so the response handling above is per the documentation rather than a captured response.

Frequently asked questions

FAQ

Because the httpx proxies argument was removed in 0.28.0 (released 2024-11-28) after being deprecated in 0.26.0. Rename it to proxy= for a single proxy, or use mounts= with httpx.HTTPTransport(proxy=...) for per-scheme or per-host routing.

Only as a stopgap when a dependency you can't change still passes proxies=. Pin httpx<0.28 until that dependency updates; for your own code, the rename to proxy= is a one-line change and keeps you on the current release.

HTTPS requests go through a CONNECT tunnel, and when the proxy refuses the tunnel httpx raises httpx.ProxyError: 407 Proxy Authentication Required. Plain HTTP requests are forwarded, so the 407 arrives as a normal response object and nothing is raised unless you call raise_for_status().

Install the extra with pip install "httpx[socks]", then pass proxy="socks5://user:pass@host:port". Without the extra, httpx raises an ImportError naming the missing socksio package as soon as the client is built.

Either the proxy URL points at the wrong kind of port (an http:// URL on a SOCKS port fails every time), or a reused connection died while idle. HTTP/2 is more exposed to the second case; turning http2 off, lowering keepalive_expiry or retrying the request in your own code fixes it.

Yes, when you don't pass proxy=, mounts or transport=, and trust_env is left at its default of True. Passing proxy= ignores the environment completely, including NO_PROXY, so express exclusions as None mounts instead.

Special Discount ยท 20% off

Get 20% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Save up to 15% more on quarterly, half-yearly and yearly plans

Claim Discount

About the Author

The SparkProxy Technical Team builds and runs SparkProxy's proxy network and the SparkProxy Scraping API, and supports developers wiring them into Python, Node.js and browser automation stacks. We test the code in our guides against the live gateway at gateway.sparkproxy.io and note the library versions we used, so you can tell whether a fix applies to your install.

Keep reading

Related articles