Guide

Scrapy TunnelError: "Could Not Open CONNECT Tunnel With Proxy" (407, 403, 502) Explained

Scrapy's TunnelError means the proxy answered the CONNECT request with a status other than 200. What each status means, the Proxy-Authorization trap, and the fix.

HProxy Team··6 min read
HProxy.Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

See plans & pricing

The traceback ends with a line that tells you everything, if you know how to read it: scrapy.core.downloader.handlers.http11.TunnelError: Could not open CONNECT tunnel with proxy 203.0.113.7:8080 [{'status': 407, 'reason': b'Proxy Authentication Required'}]. The address is the proxy Scrapy tried. The dictionary is the proxy's answer to Scrapy's request for a tunnel. And the status inside it is the whole diagnosis, because a tunnel request has only a few ways to fail and each one has its own number.

Scrapy raises this for https:// URLs, and only for those. For a plain http:// page, Scrapy sends the request to the proxy and the proxy fetches it. For an HTTPS page, Scrapy first sends CONNECT target:443 to the proxy, asking it to open a raw connection it can then encrypt end to end. If the proxy answers anything but 200, the tunnel never opens, the target is never reached, and Scrapy has nothing to hand to the spider except this exception. That is why the error is about the proxy and not the site, why the site's anti-bot systems are not involved, and why retrying with the same proxy and the same credentials produces the same status every time.

Reading the status

Status in the TunnelErrorWhat the proxy saidUsual causeRight response
407Authenticate firstNo credentials sent, wrong ones, or your address is not on an IP-authenticated proxy's allowlistFix credentials or the allowlist; do not retry
403Tunnel forbiddenPort or destination not permitted, or a gateway refusing the country or sessionChange the port, target, or proxy; do not retry
400Malformed tunnel requestA bad proxy URL or a target with an odd hostname or portFix the request
429The proxy is rate-limiting youToo many concurrent connections through one proxy or accountLower concurrency per proxy
502Could not reach the targetTarget unreachable from the proxy, or no exit matched the requestRetry on a different exit
503No capacity, or no exit availableOverloaded proxy, or a gateway with nothing free for that country or sessionRetry later or on another exit
504The proxy timed out reaching the targetSlow or dropping targetRetry, with a longer timeout on the proxy side if configurable

Two families, in other words. 4xx statuses are the proxy refusing you, and they repeat on retry, so the fix is configuration. 5xx statuses are the proxy failing to reach the world on your behalf, and they are worth retrying, ideally somewhere else.

Where Scrapy gets the proxy, and the credentials

Scrapy's HttpProxyMiddleware reads the proxy from two places: request.meta['proxy'], which a rotator or the spider sets per request, and the http_proxy and https_proxy environment variables, which apply to everything. The proxy value is a URL, and credentials belong inside it:

request.meta["proxy"] = "http://user:p%40ss@gateway.example:8080"

The middleware strips the credentials out of the URL and sends them as a Proxy-Authorization header on the request. That is the only way credentials should reach the proxy; setting the header by hand invites the trap below. Special characters in the password are URL-encoded, so p@ss is written p%40ss, or the URL parses at the wrong @ and the proxy sees a mangled username.

For a gateway that selects country or session from the username, the username carries that too, and a typo there produces a 407 or a 403 just as a wrong password does. Our Scrapy proxy guide covers the middleware order and the rotator that sets meta['proxy'] per request.

The Proxy-Authorization trap

Because the header is derived from the URL and stored on the request, a rotator that changes request.meta['proxy'] on a retry can leave the previous proxy's header in place, and the new proxy receives credentials meant for the old one. The symptom is a 407 that appears only on retried requests, or only after the pool rotates, while a fresh request through the same proxy works. Recent Scrapy releases clear the header when the proxy URL changes without credentials; on older releases, and as a habit, a rotator that changes the proxy should delete the header:

request.headers.pop(b"Proxy-Authorization", None)
request.meta["proxy"] = next_proxy_url_with_credentials

Never set Proxy-Authorization yourself for a pool whose proxies have different credentials. Put the credentials in each proxy's URL and let the middleware do the rest.

Retrying the right statuses

Scrapy's RetryMiddleware treats TunnelError as a retryable exception, so a 407 gets retried RETRY_TIMES times with the same result, which wastes attempts and hides the real problem in the log. A downloader middleware that reads the status from the exception and decides per status is a few lines:

import re
from scrapy.core.downloader.handlers.http11 import TunnelError

class TunnelErrorMiddleware:
    STATUS = re.compile(r"'status': (\d{3})")

    def process_exception(self, request, exception, spider):
        if not isinstance(exception, TunnelError):
            return None
        m = self.STATUS.search(str(exception))
        status = int(m.group(1)) if m else 0
        proxy = request.meta.get("proxy")
        if status in (407, 403, 400):
            spider.logger.error("proxy %s refused the tunnel with %s; fix configuration", proxy, status)
            return None  # let it fail; do not burn retries on a repeatable refusal
        # 5xx and 429: rotate and retry
        request.headers.pop(b"Proxy-Authorization", None)
        request.meta["proxy"] = spider.next_proxy()  # your rotator
        request.dont_filter = True
        return request

Place it in DOWNLOADER_MIDDLEWARES after the proxy middleware's order so that it sees the exception, and give the spider a next_proxy() that returns a proxy URL with credentials. The effect is that a 407 fails fast and loudly, a 502 moves to another exit, and the log tells you which happened.

When it is not the proxy's fault

A 502 or 503 in the tunnel from every proxy in the pool against one target means the target is refusing or dropping connections from the proxy network as a whole, or is down. Rotation cannot fix that; a different kind of address can, if the target is dropping datacenter ranges, and our guide on how websites detect proxies covers the address side. A 502 from one proxy against everything means that proxy's upstream is broken, and it belongs out of the pool. Free proxies produce both constantly, which is why a spider on a public list spends most of its time in this exception.

For the equivalent error in a browser, where Chrome reports the same failed CONNECT as ERR_TUNNEL_CONNECTION_FAILED, our tunnel error guide shows how to read the proxy's answer with curl, which is also the quickest way to test a proxy URL before a spider uses it:

curl -v -x http://user:pass@gateway.example:8080 https://example.com/ -o /dev/null

The status after CONNECT in curl's output is the status Scrapy would put in the TunnelError.

Sort by status

Read the number in the brackets. 407 and 403 are configuration: credentials in the proxy URL, encoded, the address on the allowlist, and a port or target the proxy permits; retrying them is waste. 502, 503, and 504 are the proxy failing to reach the target: rotate and retry. Clear Proxy-Authorization when the proxy changes. And if every exit gets a 502 from one site, the site is dropping the network, not the spider. A residential pool with sticky sessions, credentials in the URL, and a middleware that sorts the statuses is the setup that keeps this exception rare; the Scrapy use-case page covers sizing it.

Frequently asked questions

What does Scrapy TunnelError Could not open CONNECT tunnel with proxy mean?
For an https:// URL, Scrapy asks the proxy to open a tunnel to the target with an HTTP CONNECT request, and the proxy answered with a status other than 200. Scrapy reports that as scrapy.core.downloader.handlers.http11.TunnelError and prints the status and reason the proxy sent, for example {'status': 407, 'reason': b'Proxy Authentication Required'}. The number in the brackets is the diagnosis; the target site was never reached.
How do I fix TunnelError with status 407?
The proxy wants credentials that Scrapy did not send. Put them in the proxy URL, http://user:password@host:port, either in request.meta['proxy'] or in the https_proxy environment variable, and Scrapy's HttpProxyMiddleware turns them into the Proxy-Authorization header. URL-encode special characters. If the proxy is IP-authenticated, the 407 means the machine's address is not on the allowlist. A 407 repeats identically on retry, so retrying is wasted time until the credentials or allowlist are fixed.
Why do I get TunnelError with status 403 from the proxy?
The proxy refused to open the tunnel as a matter of policy: the destination port is not allowed, the target is on the proxy's deny list, the address is not permitted to use this proxy for that target, or a gateway refuses a country or session that does not exist. A 403 at the CONNECT stage comes from the proxy, not the website, and the fix is a different port, target, or proxy rather than a retry.
What do 502 and 503 in a TunnelError mean?
The proxy accepted the tunnel request and could not connect onward: the target was unreachable from the proxy, or, on a rotating gateway, no exit was available that matched the request's country or session at that moment. These are the statuses worth retrying, ideally on a different exit. A 502 from every proxy in the pool against one target means the target is down or dropping the proxy network, and rotation will not change that.
Does Scrapy retry TunnelError automatically?
Yes. TunnelError is in the retry middleware's list of exceptions to retry, so Scrapy retries it up to RETRY_TIMES. That is right for a 502 or 503 from an exhausted exit and wrong for a 407 or 403, which return the same answer every time. A small downloader middleware that reads the status out of the exception and decides per status, retry with a new proxy for 5xx, stop and log for 407 and 403, saves the wasted attempts.
Why does the Proxy-Authorization header stick to the wrong proxy?
Scrapy derives Proxy-Authorization from the credentials in the proxy URL and keeps the header on the request. When a rotator switches request.meta['proxy'] to a different proxy with different credentials, an older header can survive and be sent to the new proxy, which answers 407. Recent Scrapy versions clear the header when the proxy changes; on older ones, delete request.headers['Proxy-Authorization'] whenever you change the proxy, and always carry credentials in the proxy URL rather than setting the header yourself.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup