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.

7 min readTested on Scrapy 2.19 · Python 3.12
Scrapy reports a refused proxy tunnel as TunnelError: Could not open CONNECT tunnel, with the proxy's status inside: 407, 403 or 465.The number in the brackets is the proxy’s answerTunnelError: Could not open CONNECT tunnelwith proxy via.proxies.vision:9000[{'status': 407, 'reason': …}]407login missing or wrong403a value the proxy can’t read465no exit for these filtersThe website was never contacted.Scrapy 2.19 · October 14, 2026

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.

407 · login403 · username value

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:

txt
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 brokeWhat Scrapy reportedFix
Wrong password, or no login at allTunnelError … 'status': 407Copy both values again
A username value the proxy can’t read (-country-usa)TunnelError … 'status': 403Two-letter country, city written together
A session label on the rotating portTunnelError … 'status': 403Use the sticky port, 10000
Filters no exit matches (Chicago in Germany)TunnelError … 'status': 465Drop the narrowest filter
Wrong password, http:// target407 response, Ignoring non-200 responseSame fix; the error is in the log, not raised
https:// as the proxy schemeNotImplementedError: HTTPS proxies for HTTPS destinations are not supportedWrite http://
A port where nothing listensDownloadConnectionRefusedError … Connection refusedCopy host and port from the dashboard
socks5:// as the proxy schemeDownloadTimeoutError … took longer than 25.0 secondsScrapy has no SOCKS; use the HTTP port
Fig. 1 — Scrapy 2.19 against via.proxies.vision, October 14, 2026, https:// target unless noted.

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.

403USER-country-usathree-letter code
403USER-session-a1-lifetime-5 on :9000session on the rotating port
200USER-country-deGermany

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:

txt
[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.

shop/middlewares.py
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 None
pythonshop/settings.py
BOT_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 = 30
pythonshop/spiders/ip.py
import 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?
Scrapy asked the proxy to open a tunnel to an https:// site and the proxy refused. The status in the brackets is the proxy's answer, not the website's: 407 is the login, 403 a username value the proxy can't read, 465 on our gateway means no exit matches your filters.
Does Scrapy support SOCKS5 proxies?
No. Scrapy's downloader only speaks HTTP proxies. A socks5:// proxy URL raises no clear error: in our test the request simply timed out after DOWNLOAD_TIMEOUT. Use the HTTP port of the same proxy with the same login.
Can I put the proxy login into a Proxy-Authorization header in Scrapy?
Yes. Unlike Python requests, Scrapy sends a Proxy-Authorization header from the request with the CONNECT request, and our test got 200 that way. Credentials in meta['proxy'] are simpler: HttpProxyMiddleware turns them into that header for you.
Why does Scrapy retry a 407 three times?
RetryMiddleware treats TunnelError like any network error and retries it, twice by default. A wrong password or an unreadable username fails the same way each time, so the retries only slow you down. Lower RETRY_TIMES, and read the status in the log.
Why does Scrapy use a proxy I never set?
HttpProxyMiddleware reads http_proxy and https_proxy from the environment. In our test a request without meta['proxy'] went out through the proxy in https_proxy. Unset the variable, or set HTTPPROXY_ENABLED = False if you don't want that.

Residential proxies, pay per GB

HTTP and SOCKS5. Country, city and sticky sessions in the username. No subscription.