Requests ProxyError
Read the inner cause before deciding. Most of the time it is the proxy URL or the credentials in it; a 502 inside points at the path to the site.
- Usually from
- Your proxy URL; the inner cause says which part
- Try first
- Read the innermost exception in the message
What a ProxyError means through a proxy
A typical message reads ProxyError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: / (Caused by ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 407 Proxy Authentication Required'))). Max retries exceeded shows up even when nothing was retried: it is how urllib3 words its final error, and Requests does not retry by default.
The host in HTTPSConnectionPool(...) is the site you asked for, not the proxy, because for an https:// URL urllib3 keeps a pool per site and tunnels through the proxy. For an http:// URL the message says HTTPConnectionPool and names the proxy itself.
The inner cause is what matters. Connection refused or Failed to resolve means Python never reached the proxy. Tunnel connection failed: 407 Proxy Authentication Required means the proxy rejected the login. Tunnel connection failed: 502 means the proxy could not reach the site. WRONG_VERSION_NUMBER means the proxy URL starts with https://.
Your credentials, the proxy, or the target?
Read the inner cause before deciding. Most of the time it is the proxy URL or the credentials in it; a 502 inside points at the path to the site.
Your credentials
When the inner cause says
407. Usually a special character in the password that broke the URL, or a newline read in with it.The proxy
When it says
Connection refusedor fails to resolve the proxy host: the address in your config. When it says502or503, the gateway could not reach the site through the exit it picked.The target site
Only behind a
502or503in the inner cause. A site that answers at all gives you aResponse, not aProxyError.
How to tell
- Search the message for
Caused by. Everything before it is packaging. - urllib3 1.26 says
Cannot connect to proxy.where urllib3 2 saysUnable to connect to proxy. Same meaning, older library. err.args[0].reason.original_erroris the inner exception as an object, so code can branch on it instead of parsing the text.- A password that works with
curl --proxy-userbut fails inside the proxy URL has a character that needs encoding.
Fixes, in the order to try them
- Costs nothing
Read the innermost cause
Go to the last
Caused byand read the exception inside it.Connection refused,407,502andWRONG_VERSION_NUMBEReach point somewhere different, and the fixes below follow them. - Costs nothing
Percent-encode the password
A password containing
@,:,/,#or%breakshttp://user:pass@host:port. Build the URL withurllib.parse.quote(password, safe=""), which turns@into%40. Requests decodes it again before sending. - Costs nothing
Fix the scheme and the address
The proxy URL starts with
http://under both thehttpandhttpskeys, and host and port match the dashboard exactly. Print the URL with the password masked to check. - Costs nothing
Stop the environment adding a proxy
Requests fills any key you did not set from
HTTP_PROXYandHTTPS_PROXY, and on Windows and macOS from the system settings. Settrust_env = Falseon aSessionto use only the proxies you pass. - Costs some time
Retry what deserves a retry
A
502or503inside the error is often one bad exit, and a second attempt on the rotating gateway leaves from another. Retry those once or twice. A407orConnection refusedfails the same way every time, so fix it instead.
See the real status and headers
Each sample digs out the inner cause, or its equivalent in that tool, which is the only part of the message worth reading.
curl
# the same credentials outside Python: if this works, the URL in your code is the problem
curl -s -o /dev/null -w 'CONNECT %{http_connect} site %{http_code}\n' \
-x http://resi.proxymonkey.io:8000 --proxy-user 'USER:PASS' https://example.com/
# the encoded form of a password, ready for a proxy URL
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' 'PASS'Python Requests
import os
from urllib.parse import quote
import requests
user = os.environ["PM_USER"].strip()
password = quote(os.environ["PM_PASS"].strip(), safe="")
PROXY = f"http://{user}:{password}@resi.proxymonkey.io:8000"
try:
r = requests.get("https://example.com/", proxies={"http": PROXY, "https": PROXY}, timeout=30)
print("through the proxy:", r.status_code)
except requests.exceptions.ProxyError as err:
reason = err.args[0].reason
inner = getattr(reason, "original_error", reason)
print(type(inner).__name__, inner)
# NewConnectionError ... Failed to establish a new connection: [Errno 111] Connection refused
# OSError Tunnel connection failed: 407 Proxy Authentication Required
# OSError Tunnel connection failed: 502 Bad GatewayScrapy
from scrapy.core.downloader.handlers.http11 import TunnelError
from scrapy.exceptions import (
CannotResolveHostError,
DownloadConnectionRefusedError,
DownloadFailedError,
DownloadTimeoutError,
)
CAUSES = {
DownloadConnectionRefusedError: "refused at the proxy address",
CannotResolveHostError: "a hostname did not resolve",
TunnelError: "the proxy answered CONNECT with an error",
DownloadTimeoutError: "no answer in time",
DownloadFailedError: "the connection failed after it opened",
}
async def start(self):
for url in self.start_urls:
yield scrapy.Request(url, errback=self.on_error)
def on_error(self, failure):
for kind, meaning in CAUSES.items():
if failure.check(kind):
self.logger.error("%s: %s", meaning, failure.value)
return
self.logger.error("%r", failure.value)Scrapy has no ProxyError. The same causes arrive as these exceptions, and a TunnelError carries the status the proxy sent.
Playwright
from playwright.sync_api import Error, sync_playwright
PROXY = {"server": "http://resi.proxymonkey.io:8000", "username": "USER", "password": "PASS"}
CAUSES = {
"ERR_PROXY_CONNECTION_FAILED": "never reached the proxy",
"ERR_TUNNEL_CONNECTION_FAILED": "the proxy refused CONNECT",
"ERR_TIMED_OUT": "no answer in time",
}
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY)
page = browser.new_page()
try:
page.goto("https://example.com/")
except Error as err:
cause = next((c for c in CAUSES if c in err.message), None)
print(CAUSES.get(cause, err.message.splitlines()[0]))
browser.close()Playwright takes the password in its own password field, so it needs no URL encoding.
Node.js
import { fetch, ProxyAgent } from "undici";
const user = encodeURIComponent(process.env.PM_USER.trim());
const pass = encodeURIComponent(process.env.PM_PASS.trim());
const dispatcher = new ProxyAgent("http://" + user + ":" + pass + "@resi.proxymonkey.io:8000");
try {
await fetch("https://example.com/", { dispatcher });
} catch (err) {
const chain = [];
for (let e = err; e; e = e.cause) chain.push(e.code ?? e.message);
console.error(chain.join(" <- "));
}
// fetch failed <- ECONNREFUSEDundici decodes the username and password in the proxy URL before sending them, so encoding them here is safe.
The samples use the residential gateway, resi.proxymonkey.io:8000. For an ISP or datacenter IP, use USER:PASS@IP:PORT for the address you rented. Your dashboard lists the host and port for every order, and where it differs from this page, the dashboard is right.
Will a different proxy line fix it?
When switching helps
Only when the inner cause is a scattered 502 from residential exits on one target. A static ISP or datacenter IP gives a steadier path, if the target accepts that kind of address.
When it will not
A refused connection, a 407 or an https:// proxy URL is in your config, and follows you to every line.
Is a failed request billed?
We bill for request bytes and response bytes, including headers and protocol overhead on the tunnelled connection. Connections that fail before transferring data are not billed. Retries that you initiate are billed and appear as separate entries in your usage log.
The terms say connections that fail before transferring data are not billed. A refused connection or a failed handshake with the proxy moves nothing through it. For a tunnel the proxy refused, check the row in your usage log and ask in Discord if it looks wrong. Retries you initiate are billed as separate entries.
Residential is billed per GB of that traffic. ISP and datacenter addresses are charged per IP for their term, and where a plan includes a traffic allowance, traffic past it is billed per GB under the same rule. The usage log in your dashboard has one row per request with bytes in, bytes out and cost, so you can look up the failed request yourself.
Questions people ask about a ProxyError
Why does it say Max retries exceeded when I never retried?
That is how urllib3 words the final error, whether or not any retries happened. Requests does not retry by default, so it usually means one attempt failed. The real reason is further along the message, after Caused by.
Why does the error name the website and not my proxy?
For an https URL, urllib3 keeps a connection pool per site and tunnels to it through the proxy, so the message names the site. The inner cause tells you whether the failure was on the way to the proxy or at the proxy.
My password has special characters. What do I do?
Encode it with urllib.parse.quote and safe set to an empty string before putting it in the proxy URL. An @ becomes %40 and a colon %3A. Requests decodes it again before sending, so the proxy sees your real password.
Is Unable to connect to proxy the proxy being down?
Not necessarily. urllib3 uses those words for every failure before a working tunnel exists, including a 407 for a wrong password and a 502 when the proxy could not reach the site. Read the exception inside it.
Errors that travel with this one
- 407
407 Proxy Authentication Required
The proxy did not accept your login. Nearly always the credentials, or the way your tool sends them.
Work it out → - WRONG_VERSION_NUMBER
SSL: wrong version number
Your client spoke TLS to something speaking plain HTTP. Almost always an https:// proxy URL that should say http://.
Work it out → - ECONNREFUSED
Connection refused
Something said no before any HTTP happened. Usually the host or port you gave for the proxy.
Work it out →
Still stuck on a ProxyError?
Paste your error in the Discord: the full message plus the command or the few lines that set up the proxy, with the password taken out. Someone there has seen it before.
Join the Discord4,200+monkeys in the Discord
Help from humans
Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.
A status bot that tells on us
Pool health, incidents and maintenance posted automatically. Including the bad days.
Deals and free traffic
Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.