If you are scraping websites or automating requests through proxies in Python, sooner or later you will encounter three classic errors: ProxyError,
SSLError: WRONG_VERSION_NUMBER, and HTTP
407 Proxy Authentication Required. In this article, we will analyze
the cause of each of them and show working code to fix them.
How requests work with proxies: basic syntax
The requests library accepts proxies through a dictionary
proxies, where the keys are the request schemes
(http and https),
and the values are the proxy server URLs with optional authentication.
import requests
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(response.json())
Note: even for HTTPS requests, the proxy scheme is usually specified as
http://, because most proxy servers accept
a CONNECT tunnel over the HTTP protocol, rather than wrapping it in SSL. If you specify
https:// for a proxy that does not support this — you
will encounter a SSLError, which will be analyzed below.
If you are working with SOCKS5 proxies, you will need an additional package:
pip install requests[socks]
# And the syntax for proxies changes:
proxies = {
"http": "socks5://user:password@proxy_host:port",
"https": "socks5://user:password@proxy_host:port",
}
ProxyError: causes and solutions
requests.exceptions.ProxyError is a wrapper around low-level
network exceptions that occur when trying to establish a connection with a proxy server. The most common scenarios are
four.
1. Proxy is unavailable or address/port are incorrect
requests.exceptions.ProxyError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by ProxyError('Cannot connect to proxy.',
ConnectionResetError(104, 'Connection reset by peer')))
Check that the host and port are entered correctly, and that the proxy server is actually up. The quickest way to diagnose is
to try connecting via curl:
curl -x http://user:password@proxy_host:port https://httpbin.org/ip -v
2. Proxy has blocked your request (IP or User-Agent ban)
Datacenter proxies are often banned by target websites because their IP ranges are known to anti-fraud systems. If the error occurs consistently when accessing a specific site (for example, Wildberries, Ozon, or social networks), rather than all sites at once — the issue is not with the proxy itself, but with its reputation. In this case, switching to residential proxies can help — they use real user IPs and are less likely to be blocked by anti-fraud systems.
3. Limit of simultaneous connections exceeded
If you are running multithreaded scraping and opening hundreds of connections through the same proxy, the server may
start dropping excess connections. The solution is to use a connection pool via
requests.adapters.HTTPAdapter with limits on
pool_maxsize and
max_retries:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(max_retries=retries, pool_connections=20, pool_maxsize=20)
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
4. Timeout on the proxy server side
If the proxy "hangs" longer than the set timeout, you will see a
ProxyError wrapped in
ReadTimeoutError. Increase the timeout to a reasonable value
(usually 15-30 seconds is sufficient) and add separate exception handling:
try:
response = requests.get(url, proxies=proxies, timeout=20)
except requests.exceptions.ProxyError as e:
print(f"Proxy issue: {e}")
except requests.exceptions.Timeout:
print("Proxy did not respond in the allotted time")
SSLError WRONG_VERSION_NUMBER: problem analysis
This error is one of the most confusing at first glance, but in reality, it almost always has one cause.
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL:
WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)')))
WRONG_VERSION_NUMBER means that the client attempted to perform
an SSL/TLS handshake where the server expects regular HTTP traffic (without encryption). Most often, the cause lies in an incorrectly
specified proxy scheme in the proxies dictionary.
Common mistake: specifying https:// instead of
http:// in the proxy server address. Most HTTP(S) proxies
accept connections over regular HTTP, even when proxying HTTPS traffic through the CONNECT mechanism.
# INCORRECT — will cause WRONG_VERSION_NUMBER
proxies = {
"http": "https://user:password@proxy_host:port",
"https": "https://user:password@proxy_host:port",
}
# CORRECT
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
A second common cause is a port that physically does not support TLS. For example, if the proxy provider has allocated port 8080 only for HTTP traffic, and you attempt to establish a TLS connection directly (bypassing CONNECT), the server will respond with plain text instead of a TLS handshake, and the OpenSSL client library will throw this error. Check your proxy provider's documentation for which ports and protocols (HTTP/HTTPS/SOCKS5) are available on each individual port.
A third option is that you are using socket or third-party
libraries for a direct TCP connection to the proxy, bypassing the standard requests mechanism. In this case, ensure that
the TLS wrapper is applied only to the actual target site, not to the connection to the proxy itself.
407 Proxy Authentication Required: authorization
The 407 response code is analogous to the standard 401, but for proxy servers. It means that the server requires authentication, and you either did not provide it or provided it in an incorrect format.
Method 1: login and password directly in the URL
proxies = {
"http": "http://login:password@proxy_host:port",
"https": "http://login:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies)
This method is the simplest, but if the login or password contains special characters (for example, @,
:, /),
they need to be escaped using urllib.parse.quote:
from urllib.parse import quote
login = quote("user@domain")
password = quote("p@ssw:ord!")
proxy_url = f"http://{login}:{password}@proxy_host:port"
proxies = {"http": proxy_url, "https": proxy_url}
Method 2: Proxy-Authorization header manually
If the proxy provider uses a non-standard authorization scheme or you need more control, you can build the header manually using base64 encoding:
import base64
import requests
login = "user"
password = "password"
credentials = base64.b64encode(f"{login}:{password}".encode()).decode()
headers = {
"Proxy-Authorization": f"Basic {credentials}"
}
proxies = {
"http": "http://proxy_host:port",
"https": "http://proxy_host:port",
}
response = requests.get(
"https://httpbin.org/ip",
proxies=proxies,
headers=headers,
timeout=10
)
Important: some proxy providers tie authentication not to the login/password, but to a specific static client IP (whitelist). If you change your IP (for example, working through a VPN or a dynamic IP from the provider), the 407 error may appear even with correct credentials — because your current external IP is not whitelisted on the proxy side. The solution is to log into the provider's personal account and update the list of allowed IPs, or switch to authentication by login/password instead of IP-whitelist.
Retry logic and exception handling
In practice, proxy errors are rarely one-off — especially during mass scraping. The correct architecture of the code should include retries with delays and logging of each error separately.
import time
import requests
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_with_retry(url, proxies, max_retries=3, backoff=2):
for attempt in range(1, max_retries + 1):
try:
response = requests.get(url, proxies=proxies, timeout=15)
response.raise_for_status()
return response
except requests.exceptions.ProxyError as e:
logger.warning(f"[Attempt {attempt}] ProxyError: {e}")
except requests.exceptions.SSLError as e:
logger.warning(f"[Attempt {attempt}] SSLError: {e}")
except requests.exceptions.HTTPError as e:
if response.status_code == 407:
logger.error("407: check login/password or proxy IP whitelist")
break
logger.warning(f"[Attempt {attempt}] HTTPError: {e}")
except requests.exceptions.Timeout:
logger.warning(f"[Attempt {attempt}] Connection timeout")
time.sleep(backoff * attempt)
raise RuntimeError(f"Failed to fetch {url} after {max_retries} attempts")
This approach saves hours of debugging: instead of crashing the script on the first network hiccup, you get a clear log indicating the specific type of error on each attempt.
Proxy rotation and dealing with bans
While ProxyError and 407 are often related to code settings, mass bans and stable connection refusals on specific domains almost always indicate an issue with the quality of the IP pool itself. For tasks such as scraping marketplaces (Wildberries, Ozon, Avito) or working with social networks, it is recommended to rotate IPs for each request or at specified intervals.
import random
proxy_pool = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
def get_random_proxy():
proxy = random.choice(proxy_pool)
return {"http": proxy, "https": proxy}
response = requests.get(url, proxies=get_random_proxy(), timeout=15)
For production parsers, it is usually more convenient to use proxies with automatic rotation on the provider's side — then you do not need to maintain a list of IPs yourself, and rotation occurs transparently through a single endpoint. In this case, residential proxies with rotation are particularly well-suited for bypassing anti-bot protection on sites with strict detection systems, while datacenter proxies are a good choice if speed of requests is important and the target site does not aggressively block datacenter ranges.
| Proxy Type | Speed | Ban Risk | Tasks |
|---|---|---|---|
| Datacenter | High | Medium-High | Fast scraping of open APIs |
| Residential | Medium | Low | Scraping marketplaces, social networks |
| Mobile | Medium-Low | Minimal | Working with strict anti-bot systems |
Session, timeouts, and SSL verification
For long-lived scripts (for example, cyclic parsers), it is advisable to use
requests.Session() instead of separate calls to
requests.get(). Session reuses TCP connections,
which reduces the load on the proxy server and decreases the number of ProxyErrors with large volumes of requests.
session = requests.Session()
session.proxies.update({
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
})
for url in urls_list:
try:
resp = session.get(url, timeout=15)
print(resp.status_code, url)
except requests.exceptions.RequestException as e:
print(f"Error on {url}: {e}")
It is also worth mentioning the verify parameter. If you are working
with a proxy that replaces the SSL certificate itself (MITM proxy for debugging or some corporate proxies),
requests may complain about certificate mismatch. Disabling verification completely
(verify=False) should only be done in a testing environment — in production,
this creates a risk of "man-in-the-middle" attacks:
# For local debugging only!
response = requests.get(url, proxies=proxies, verify=False, timeout=15)
# The correct way is to specify the path to the trusted proxy certificate
response = requests.get(url, proxies=proxies, verify="/path/to/proxy_cert.pem")
Error diagnosis checklist
If the script crashes with a proxy error, go through this list from top to bottom — in 90% of cases, the cause will be found in the first three points.
- Check the scheme in the
proxiesdictionary — it should almost always behttp://, even for HTTPS requests. - Test the proxy directly via
curl -xto rule out issues on the code side. - Ensure that the login and password do not contain unescaped special characters in the URL.
- Check if the proxy is tied to a specific IP (whitelist) instead of authorization by login/password.
- Increase the
timeoutto 15-30 seconds before considering the proxy non-functional. - If the error occurs only on a specific site — the issue is with the IP's reputation, not the code.
- Add retry logic with exponential backoff instead of single attempts.
- When mass scraping, use IP rotation instead of a single static proxy for all requests.
Conclusion
Most proxy errors in Python requests boil down to three things: an incorrect scheme in the proxy URL
(http instead of https),
missing or incorrectly formatted authorization, and the quality of the IP pool itself. ProxyError usually indicates a connection
issue or IP ban, SSLError WRONG_VERSION_NUMBER almost always points to an incorrect proxy scheme, and 407 —
indicates a problem with login/password or whitelist.
If you regularly encounter bans and 407 errors when scraping marketplaces or social networks, it is worth reconsidering the proxy source itself, not just the code. For stable scraping with a low percentage of blocks, we recommend trying residential proxies — they use real IP addresses and significantly less frequently cause ProxyError on secure sites. If speed and volume of requests are prioritized over bypassing complex anti-bot systems, consider datacenter proxies — they are cheaper and faster for simple scraping tasks.