Fixing Could not open CONNECT tunnel with proxy in Scrapy
The status in the brackets is the proxy talking, not the website. Here is what each one means, the two mistakes that fail without any error, and a middleware that sets the proxy right.
In short
Read the status inside the error. 407: the login is missing or wrong. 403: a value in the username the proxy can’t read. 465 on our gateway: no exit matches your filters. Scrapy can’t use SOCKS5 at all, so point it at the HTTP port.
To fetch an https:// page through a proxy, Scrapy first sends the proxy one request: CONNECT ipinfo.io:443, with your proxy login in a Proxy-Authorization header. If the proxy agrees, it opens a tunnel and Scrapy talks TLS to the website through it. If it refuses, Scrapy raises:
scrapy.core.downloader.handlers.http11.TunnelError: Could not open CONNECT tunnel with proxy via.proxies.vision:9000 [{'status': 407, 'reason': b'Proxy Authentication Required'}]The website never saw that request. So the status is about your proxy settings, never about the site, and it is the only part of the message you need. The reason phrase after it is whatever the proxy chose to say.
For an http:// page there is no tunnel. The same refusal comes back as an ordinary response, and Scrapy’s HttpErrorMiddleware drops it: the log says Ignoring non-200 response and your callback never runs.
Each status, reproduced
We broke a working proxy setting in one way at a time and noted what Scrapy reported.
| What we broke | What Scrapy reported | Fix |
|---|---|---|
| Wrong password, or no login at all | TunnelError … 'status': 407 | Copy both values again |
| A username value the proxy can’t read (-country-usa) | TunnelError … 'status': 403 | Two-letter country, city written together |
| A session label on the rotating port | TunnelError … 'status': 403 | Use the sticky port, 10000 |
| Filters no exit matches (Chicago in Germany) | TunnelError … 'status': 465 | Drop the narrowest filter |
| Wrong password, http:// target | 407 response, Ignoring non-200 response | Same fix; the error is in the log, not raised |
| https:// as the proxy scheme | NotImplementedError: HTTPS proxies for HTTPS destinations are not supported | Write http:// |
| A port where nothing listens | DownloadConnectionRefusedError … Connection refused | Copy host and port from the dashboard |
| socks5:// as the proxy scheme | DownloadTimeoutError … took longer than 25.0 seconds | Scrapy has no SOCKS; use the HTTP port |
407: the login
The proxy got no login it accepts. Usually a typo or a password that was reset. In Scrapy the login goes into the proxy URL in request.meta["proxy"], and HttpProxyMiddleware turns it into the header. A Proxy-Authorization header you set yourself also worked in our test, unlike in Python requests, where the same header gives 407.
403: a value in the username
Targeting goes into the proxy username, and our gateway checks every value. A three-letter country code, a city with a space, a lifetime over 1440 minutes, or a session label sent to the rotating port all end in 403.
465: no exit
465 is not a standard HTTP status. Our gateway uses it when the login is fine but no residential exit matches every filter at once, for example -country-de-city-chicago. It doesn’t quietly give you another place. Remove the narrowest filter first; a 465 for a username that worked a minute ago is an exit that went offline.
socks5:// just times out
Scrapy’s downloader only speaks HTTP proxies. Give it a socks5:// URL and you get no “unsupported scheme” message: the request hangs until DOWNLOAD_TIMEOUT and fails with DownloadTimeoutError. Use the HTTP port of the same proxy with the same login. On our gateway that is 9000, or 10000 for sticky sessions.
A key the proxy doesn’t know is ignored
Copy a username from another provider’s docs and some keys won’t exist on ours. USER-cc-DE returned 200 in our test, with an exit in Jamaica. No error, just the wrong country in your data. Write -country-de, and spot-check where your exits are.
The environment sets a proxy
HttpProxyMiddleware also reads http_proxy and https_proxy. A request without meta["proxy"] went out through the proxy in https_proxy in our test. If you see a 407 from a proxy you never configured, look at env | grep -i proxy.
RetryMiddleware treats a TunnelError like a dropped connection and tries again, twice by default. For a filter that matched nothing, the log showed three attempts before Scrapy gave up:
[scrapy.downloadermiddlewares.retry] ERROR: Gave up retrying <GET https://ipinfo.io/json> (failed 3 times): Could not open CONNECT tunnel with proxy via.proxies.vision:9000 [{'status': 465, 'reason': b'Can Not Fulfill Targeting'}]A retry can help with 465 (another exit may come online) and with dropped connections. It never helps with 407 or 403: the same login fails the same way. Keep RETRY_TIMES low and fix the setting the status points to.
A setup that works
A downloader middleware that sets the proxy per request: a new IP by default, one IP per session label when you ask for it. Ran as shown.
import os
import uuid
GATEWAY = "via.proxies.vision:10000" # the sticky port: it reads a session label
class ResidentialProxyMiddleware:
"""Sets the proxy on every request. meta["country"] picks the exit country,
meta["session"] keeps one exit for that label; without it, every request
gets a fresh label and so a new IP, even on a kept-alive connection."""
def __init__(self):
self.user = os.environ["PROXY_USER"]
self.password = os.environ["PROXY_PASS"]
def process_request(self, request, spider=None):
if "proxy" in request.meta:
return None
user = self.user
if request.meta.get("country"):
user += f"-country-{request.meta['country'].lower()}"
session = request.meta.get("session") or uuid.uuid4().hex[:8]
user += f"-session-{session}-lifetime-10"
request.meta["proxy"] = f"http://{user}:{self.password}@{GATEWAY}"
return NoneBOT_NAME = "shop"
SPIDER_MODULES = ["shop.spiders"]
DOWNLOADER_MIDDLEWARES = {
"shop.middlewares.ResidentialProxyMiddleware": 350, # before HttpProxyMiddleware (750)
}
# Tunnel errors are retried by default. 403 and 407 won't change on a retry,
# so keep the count low and read the status in the log.
RETRY_TIMES = 1
DOWNLOAD_TIMEOUT = 30import json
import scrapy
class IpSpider(scrapy.Spider):
name = "ip"
async def start(self):
for i in range(3): # three requests, three German IPs
yield scrapy.Request("https://ipinfo.io/json", meta={"country": "de"}, dont_filter=True)
for i in range(2): # the same label twice: one IP
yield scrapy.Request("https://ipinfo.io/json", meta={"country": "de", "session": "account1"}, dont_filter=True)
def parse(self, response):
data = json.loads(response.text)
yield {"session": response.meta.get("session"), "ip": data["ip"], "country": data["country"]}PROXY_USER=… PROXY_PASS=… scrapy crawl ip -O ips.jsonl wrote three different German IPs, then the same IP twice for account1. Every request gets a session label because Scrapy keeps connections to the proxy open; without a fresh label they would all leave through one exit. We measured that here.
Expect the odd failure
An exit is someone’s home connection. In one of our runs a single request got a gateway error (status 568) and failed after its retry; the next run had none. That one retry is worth keeping.
Other ways to set it
For one fixed proxy, meta={"proxy": "http://USER:PASS@via.proxies.vision:9000"} on each request is enough; port 9000 gives a new exit per connection.
What does 'Could not open CONNECT tunnel with proxy' mean in Scrapy?
Does Scrapy support SOCKS5 proxies?
Can I put the proxy login into a Proxy-Authorization header in Scrapy?
Why does Scrapy retry a 407 three times?
Why does Scrapy use a proxy I never set?
Keep reading
aiohttp Proxy Errors: ClientHttpProxyError 407, SOCKS5 and trust_env
aiohttp's ClientHttpProxyError explained, why its message leaks your proxy password, socks5:// without aiohttp-socks, and rotation.
6 min read · #python#errors
Tunnel connection failed: 407 in Python requests (and 403, 465)
The status is the proxy's answer, not the website's. Five causes of 407 we reproduced, and what 403 and 465 mean instead.
7 min read · #python#errors
Missing dependencies for SOCKS support: the Fix, and Why You See It Without SOCKS
Install requests[socks], or find the environment variable that asked for SOCKS. Plus socks5 vs socks5h and the errors that come next.
6 min read · #python#socks5
Residential proxies, pay per GB
HTTP and SOCKS5. Country, city and sticky sessions in the username. No subscription.