# requests.exceptions.ProxyError: How to Fix

> What requests.exceptions.ProxyError really means, the exact message ten different root causes print, and the one-line fix for each. Measured, not guessed.

[Home](https://quanticdata.io/)/[Blog](https://quanticdata.io/blog/)/requests.exceptions.ProxyError: How to Fix

# requests.exceptions.ProxyError: How to Fix

TroubleshootingSep 8, 2026·9 min read·By [Aldo Morese](https://quanticdata.io/about/), founder of QuanticData

On this page [What ProxyError actually is](/blog/requests-exceptions-proxyerror/#what-proxyerror-actually-is) [Ten causes, and the exact message each one prints](/blog/requests-exceptions-proxyerror/#ten-causes-and-the-exact-message-each-one-prints) [The urllib3 2.x change that quietly breaks "except ProxyError"](/blog/requests-exceptions-proxyerror/#the-urllib3-2-x-change-that-quietly-breaks-except-proxyerror) [Fix it by cause](/blog/requests-exceptions-proxyerror/#fix-it-by-cause) [Why the same broken proxy sometimes returns a 407 instead of raising](/blog/requests-exceptions-proxyerror/#why-the-same-broken-proxy-sometimes-returns-a-407-instead-of) [A triage snippet that names the cause](/blog/requests-exceptions-proxyerror/#a-triage-snippet-that-names-the-cause) [When the proxy is fine and the target is the problem](/blog/requests-exceptions-proxyerror/#when-the-proxy-is-fine-and-the-target-is-the-problem)

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 wrong | What Requests raises | The part that identifies it |
| --- | --- | --- |
| Nothing is listening on the proxy port | ProxyError | `NewConnectionError ... [Errno 61] Connection refused` |
| The proxy hostname does not resolve | ProxyError | `NameResolutionError ... Failed to resolve` |
| `https://` scheme in front of an HTTP proxy | ProxyError | `Your proxy appears to only use HTTP and not HTTPS` + `[SSL: WRONG_VERSION_NUMBER]` |
| Bad or missing credentials, HTTPS target | ProxyError | `OSError('Tunnel connection failed: 407 Proxy Authentication Required')` |
| Bad or missing credentials, plain HTTP target | **nothing — a 407 response** | no exception at all; `r.status_code == 407` |
| The port answers HTTP but is not a proxy | ProxyError | `OSError('Tunnel connection failed: 400 Bad Request')` |
| The proxy accepts the socket, then closes it | **ConnectionError** | `('Connection aborted.', ConnectionResetError(54, ...))` |
| The proxy accepts the socket and never answers | **ReadTimeout** | `Read timed out. (read timeout=4)` |
| `socks5://` without the SOCKS extra installed | **InvalidSchema** | `Missing dependencies for SOCKS support.` |
| An `HTTPS_PROXY` variable you forgot was exported | ProxyError | identical 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.

| Scenario | urllib3 1.26.20 | urllib3 2.0.7 and 2.7.0 |
| --- | --- | --- |
| Proxy accepts the socket, then resets it | `ProxyError('Cannot connect to proxy.', ConnectionResetError)` | `ConnectionError('Connection aborted.', ConnectionResetError)` |
| Proxy accepts the socket, never answers CONNECT | `ProxyError('Cannot connect to proxy.', TimeoutError)` | `ReadTimeout: Read timed out.` |
| Wording of every other proxy failure | `Cannot 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](https://quanticdata.io/blog/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](https://quanticdata.io/tools/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](https://quanticdata.io/blog/how-to-rotate-proxies-python/); if every exit fails the same way, work through the [proxy not working checklist](https://quanticdata.io/blog/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](https://www.rfc-editor.org/rfc/rfc9110.html#status.407) 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](https://quanticdata.io/blog/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](https://quanticdata.io/blog/web-scraping-403-forbidden/), a [429](https://quanticdata.io/blog/429-too-many-requests-web-scraping/), 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](https://quanticdata.io/residential-proxies/) start at $0.80/GB and leave you owning retries, rotation and this exact error class. Our [web scraping API](https://quanticdata.io/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

- [Requests source: the exceptions module](https://requests.readthedocs.io/en/latest/_modules/requests/exceptions/)

- [urllib3: HTTPS proxy error / HTTP proxy](https://urllib3.readthedocs.io/en/latest/advanced-usage.html#https-proxy-error-http-proxy)

- [urllib3 changelog: ProxyError now wraps any connection error (2.0.0)](https://urllib3.readthedocs.io/en/stable/changelog.html)

- [RFC 9110 section 15.5.8: 407 Proxy Authentication Required](https://www.rfc-editor.org/rfc/rfc9110.html#status.407)

- [Stack Overflow: I am getting this error: requests.exceptions.ProxyError](https://stackoverflow.com/questions/74213976/i-am-getting-this-error-requests-exceptions-proxyerror)

- [psf/requests issue 3558: Using Python's requests lib throwing ProxyError](https://github.com/psf/requests/issues/3558)

## FAQ

Quick answers on requests.exceptions.proxyerror.

[Something else? Ask us →](mailto:hello@quanticdata.io)

### 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.

[Start free — $2/month included](https://quanticdata.io/signup/)[Explore Residential Proxies from $0.80/GB](https://quanticdata.io/residential-proxies/)

## Related reading

[Troubleshooting How to Fix 429 Too Many Requests When Scraping A 429 is the one block that tells the truth: you crossed a rate limit. What it does not tell you is what the limit is keyed on, and that decides whether proxies help at all. A two-request test to find the key, backoff code that honours Retry-After, the concurrency formula that turns pages per hour into IPs in flight, and a per-host token bucket. Read →](https://quanticdata.io/blog/429-too-many-requests-web-scraping/) [Troubleshooting Proxy Not Working? A 10-Step Checklist “Proxy not working” is four different problems wearing one name: you cannot reach the proxy, the proxy refuses you, the proxy reaches the site but the site refuses it, or it works and is slow. Each layer has its own error strings and its own fix. A table that maps the message to the layer, curl timings that separate slow from broken, and a 10-step checklist in the order that saves the most time. Read →](https://quanticdata.io/blog/proxy-not-working-checklist/) [Troubleshooting ERR_TUNNEL_CONNECTION_FAILED: How to Fix Chromium error -111 is raised when a CONNECT request to your proxy does not come back as a usable tunnel. Here is how to tell which of the seven causes you have, in the browser and in curl, Playwright, Puppeteer and Selenium. Read →](https://quanticdata.io/blog/err-tunnel-connection-failed/)

---

Source: https://quanticdata.io/blog/requests-exceptions-proxyerror/ · Site index for AI: https://quanticdata.io/llms.txt
