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

requests.exceptions.ProxyError: How to Fix

A Python script reaches a proxy, asks it for a CONNECT tunnel, and only then reaches the target site. requests raises ProxyError for failures in the first two stages: reaching the proxy, and the proxy's answer to CONNECT. Once the tunnel is open, a block by the target arrives as an ordinary HTTP response, not an exception.

requests.exceptions.ProxyError means the connection to your proxy failed — not that the target site blocked you. The proxy was unreachable, refused the tunnel, or answered with something that is not a proxy response. Which of the three it was is written in the message, after the words "Caused by".

What ProxyError actually is

In Requests, ProxyError is a subclass of ConnectionError, which is itself a RequestException. Catching requests.exceptions.ConnectionError catches proxy failures too; catching ProxyError alone does not catch every proxy-shaped failure, which is the trap we measure below.

The message is three layers deep and almost everyone reads the wrong one:

requests.exceptions.ProxyError: HTTPSConnectionPool(host='example.com', port=443):
  Max retries exceeded with url: /
  (Caused by ProxyError('Unable to connect to proxy',
    NewConnectionError("HTTPSConnection(host='127.0.0.1', port=54056):
      Failed to establish a new connection: [Errno 61] Connection refused")))
  • HTTPSConnectionPool(host='example.com') is the site you asked for. It is not the thing that failed, and it never saw your request.
  • Max retries exceeded is urllib3's retry wrapper reporting that it gave up. It does not mean you should add retries.
  • Caused by ProxyError(...) is the real diagnosis, and the innermost exception — NewConnectionError, NameResolutionError, SSLError, OSError — is the cause you have to fix.

Ten causes, and the exact message each one prints

We ran ten isolated failure modes against a local fake proxy whose behaviour we controlled, so exactly one thing was wrong in each run. Environment: macOS, Python 3.13.6, OpenSSL 3.6.3, Requests 2.34.2 with urllib3 2.7.0, 8 September 2026. Every message below is copied from the run, not from memory.

What is actually wrongWhat Requests raisesThe part that identifies it
Nothing is listening on the proxy portProxyErrorNewConnectionError ... [Errno 61] Connection refused
The proxy hostname does not resolveProxyErrorNameResolutionError ... Failed to resolve
https:// scheme in front of an HTTP proxyProxyErrorYour proxy appears to only use HTTP and not HTTPS + [SSL: WRONG_VERSION_NUMBER]
Bad or missing credentials, HTTPS targetProxyErrorOSError('Tunnel connection failed: 407 Proxy Authentication Required')
Bad or missing credentials, plain HTTP targetnothing — a 407 responseno exception at all; r.status_code == 407
The port answers HTTP but is not a proxyProxyErrorOSError('Tunnel connection failed: 400 Bad Request')
The proxy accepts the socket, then closes itConnectionError('Connection aborted.', ConnectionResetError(54, ...))
The proxy accepts the socket and never answersReadTimeoutRead timed out. (read timeout=4)
socks5:// without the SOCKS extra installedInvalidSchemaMissing dependencies for SOCKS support.
An HTTPS_PROXY variable you forgot was exportedProxyErroridentical to the first row — with no proxies= in your code

Three results are worth stating plainly. Only six of the ten raise ProxyError on a current stack. One raises nothing at all. And the two that changed class — reset and silence — used to be ProxyError.

The urllib3 2.x change that quietly breaks "except ProxyError"

We re-ran the same ten scenarios on Requests 2.28.2 with urllib3 1.26.20, and again on urllib3 2.0.7. The 2.0.7 and 2.7.0 results are identical to each other, so the behaviour changed at the 2.0 boundary and has been stable since.

Scenariourllib3 1.26.20urllib3 2.0.7 and 2.7.0
Proxy accepts the socket, then resets itProxyError('Cannot connect to proxy.', ConnectionResetError)ConnectionError('Connection aborted.', ConnectionResetError)
Proxy accepts the socket, never answers CONNECTProxyError('Cannot connect to proxy.', TimeoutError)ReadTimeout: Read timed out.
Wording of every other proxy failureCannot connect to proxy.Unable to connect to proxy (no full stop)

urllib3's 2.0.0 changelog says ProxyError was changed to wrap any connection error — timeout, TLS, DNS — that occurs when connecting to the proxy. Our runs show where that boundary now sits: failures while the socket to the proxy is being established stay ProxyError; failures after it is open belong to the request, and surface as ConnectionError or ReadTimeout.

Two practical consequences. If your retry or rotation code catches ProxyError only, upgrading urllib3 silently stops retrying the two most common symptoms of an overloaded proxy pool — catch requests.exceptions.RequestException, or ConnectionError and Timeout together. And if you string-match the message, matching "Cannot connect to proxy." matches nothing on a modern install.

Fix it by cause

The https:// scheme in front of an HTTP proxy

This is the single most common self-inflicted case, and the one behind the top-ranked Stack Overflow question for this error. Both keys of the dict take an http:// URL. The key selects which target scheme uses the proxy; the value is how you talk to the proxy itself, and almost every commercial proxy speaks plaintext HTTP on its endpoint:

proxies = {
    "http":  "http://USER:PASS@gate.example.net:8080",
    "https": "http://USER:PASS@gate.example.net:8080",   # http://, not https://
}
r = requests.get("https://example.com/", proxies=proxies, timeout=20)

407 Proxy Authentication Required

The tunnel got as far as the proxy and was rejected. Check the credentials, then check for an unencoded special character in the password — a raw @, : or # silently truncates the URL — with urllib.parse.quote(password, safe=""). If your provider authenticates by IP allowlist, the machine's egress IP is what matters, not your laptop's. We took that error apart separately in how to fix 407 Proxy Authentication Required.

Wrong host, wrong port, or a port that is not a proxy

Connection refused means the TCP handshake failed: nothing is listening there, or a firewall dropped it. Tunnel connection failed: 400 Bad Request means something is listening and it answered HTTP — usually a web server, a dashboard, or the provider's API port rather than the proxy port. Confirm the endpoint with the provider's docs before touching your code, and sanity-check it in a browser-free way with our proxy tester.

Environment variables you did not set

Requests reads HTTP_PROXY, HTTPS_PROXY and ALL_PROXY from the environment, so a stale export in a shell profile, a CI runner or a Docker image produces this error in code that mentions no proxy at all. Pass proxies= explicitly, or switch the whole session off with session.trust_env = False. To bypass a proxy for one call, pass proxies={"http": None, "https": None}.

SOCKS

Missing dependencies for SOCKS support. is an InvalidSchema, not a ProxyError — the scheme was rejected before any socket was opened. Install the extra with pip install "requests[socks]" and prefer socks5h:// over socks5:// so DNS is resolved at the proxy instead of leaking your resolver.

A pool that is simply exhausted

Resets and stalled CONNECTs are what a saturated or dying endpoint looks like from the client. Retry on a fresh exit rather than on the same one, which is the pattern in rotating proxies in Python; if every exit fails the same way, work through the proxy not working checklist before blaming the code.

Why the same broken proxy sometimes returns a 407 instead of raising

An HTTPS request through a proxy starts with a CONNECT that asks for a blind tunnel. If the proxy refuses, there is no tunnel and therefore no HTTP response to give you — urllib3 turns the refusal into an exception. A plain http:// request needs no tunnel: the proxy is just a forwarder, so its 407 comes back as an ordinary response and Requests hands you a Response object with status_code == 407, exactly as RFC 9110 defines it.

So the same misconfiguration is an exception on HTTPS and a silent 407 body on HTTP. Code that only wraps requests in try/except will process that 407 page as if it were data. Check r.status_code as well as catching exceptions. The browser-side twin of this failure, when the tunnel dies inside Chrome or Playwright instead of Python, is ERR_TUNNEL_CONNECTION_FAILED.

A triage snippet that names the cause

Paste this in place of a bare except and it will tell you which row of the table you are in:

import requests
from requests.exceptions import ProxyError, ConnectionError, Timeout, InvalidSchema

SIGNS = [
    ("only use HTTP and not HTTPS", "proxy URL scheme: use http:// in both dict keys"),
    ("407",                          "proxy credentials or IP allowlist"),
    ("Tunnel connection failed: 4",  "that port answers HTTP but is not a proxy"),
    ("Connection refused",           "wrong port, or the proxy is down"),
    ("Failed to resolve",            "wrong proxy hostname"),
    ("Name or service not known",    "wrong proxy hostname"),
]

def diagnose(url, proxies):
    try:
        r = requests.get(url, proxies=proxies, timeout=15)
        if r.status_code == 407:
            return "407 returned as a response: proxy auth failed on a plain HTTP target"
        return "ok: HTTP " + str(r.status_code)
    except InvalidSchema as e:
        return "scheme not supported: " + str(e)
    except (ProxyError, ConnectionError) as e:
        text = str(e)
        for needle, verdict in SIGNS:
            if needle in text:
                return verdict
        return "connection to the proxy failed: " + text[:200]
    except Timeout:
        return "proxy accepted the socket but never answered - treat as a dead exit"

print(diagnose("https://example.com/", {"http": "...", "https": "..."}))

Run it against an http:// target and an https:// target. If only the HTTPS one fails, the problem is the tunnel — credentials or a proxy that does not allow CONNECT to port 443.

When the proxy is fine and the target is the problem

Once the tunnel is open, the proxy is out of the picture and blocks arrive as ordinary status codes: a 403, a 429, or a challenge page with a 200. No amount of proxy debugging fixes those, and no ProxyError will ever be raised for them. The split is worth internalising: exceptions are your side of the tunnel, status codes are theirs.

If the debugging above is a recurring tax rather than a one-off, the layer choice is the real fix. Raw exits from residential proxies start at $0.80/GB and leave you owning retries, rotation and this exact error class. Our web scraping API takes a URL and returns the page from $0.0002/page, pay per success: no proxies dict, no CONNECT, and a failed fetch is not billed. Both bill from the same $2 of free monthly credit, so you can put the two paths side by side before choosing.

Sources & further reading

FAQ

Quick answers on requests.exceptions.proxyerror.

Something else? Ask us →

Is ProxyError the same as ConnectionError?

ProxyError is a subclass of requests.exceptions.ConnectionError, so catching ConnectionError catches it — but not the reverse. On urllib3 2.x a proxy that resets the socket raises plain ConnectionError and one that stalls raises ReadTimeout, so except ProxyError alone misses two real proxy failures.

Why does the error name the website and not my proxy?

The outer HTTPSConnectionPool(host='example.com', port=443) is the pool for the URL you requested, which is how urllib3 labels every failure on that request. The proxy's host and port appear in the inner exception, after "Caused by". The site itself never received anything.

Does "Max retries exceeded" mean I should add retries?

No. It is the standard wording urllib3 uses when its retry budget — often zero extra attempts — is spent, and it appears even on the first try. Retrying a wrong scheme, a wrong password or a wrong port just fails identically three times more slowly.

Why do I get a ProxyError when my code sets no proxy?

Requests honours HTTP_PROXY, HTTPS_PROXY and ALL_PROXY from the environment. A stale export in a shell profile, CI job or container image is picked up silently. Set session.trust_env = False, or pass proxies={"http": None, "https": None} on the call.

Should I use socks5 or socks5h in the proxy URL?

Use socks5h://. With socks5:// the hostname is resolved locally before the connection is made, which leaks the lookup to your own resolver and defeats geo-targeting; the "h" tells the client to let the proxy resolve it. Both need pip install "requests[socks]".

How do I tell a broken proxy from a blocking website?

Request an http:// URL and an https:// URL through the same proxy. Failures on both are the proxy; a failure only on HTTPS is the CONNECT tunnel, which means credentials or a port restriction; a status code coming back at all means the proxy worked and the site answered.

Skip the proxies dict entirely

Send a URL, get the page. Pay per success from $0.0002 a page, with $2 of free API usage every month — no CONNECT tunnels to debug.

Related reading