A 407 Proxy Authentication Required is sent by the proxy, not by the website you asked for. It means the proxy refused to forward your request until it sees credentials it accepts, so zero bytes reached the target. Rotating user agents or slowing down cannot fix it. The fix is always one of five things: credentials that were never sent, wrong credentials, an unencoded special character, an IP allowlist mismatch, or a plan the gateway no longer honours.
What a 407 actually says
RFC 9110 defines 407 as the proxy-side twin of 401. The proxy must send a Proxy-Authenticate header naming the scheme it wants, and the client answers with Proxy-Authorization. The two codes look alike but point at different machines, which is why mixing them up sends your debugging in the wrong direction:
| 401 Unauthorized | 407 Proxy Authentication Required | |
|---|---|---|
| Who is refusing | The website | The proxy in between |
| Challenge header | WWW-Authenticate | Proxy-Authenticate |
| What you send back | Authorization | Proxy-Authorization |
| Did the request reach the site? | Yes | No |
| Will headers or delays help? | No | No |
The scheme in the challenge is the whole diagnosis. Every commercial proxy gateway uses Basic, which is base64 of user:password. If you see NTLM or Negotiate instead, you are behind a corporate proxy, and a client that only speaks Basic will loop on 407 forever no matter how correct the password is. If the 407 carries no Proxy-Authenticate header at all, it is not a real proxy challenge: captive portals, transparent ISP proxies and some load balancers produce 407-shaped responses that no credential will satisfy.
Why on HTTPS you see a tunnel error instead of a status code
Almost every guide tells you to check response.status_code == 407. On an HTTPS target that check never runs. For plain HTTP the proxy receives the full request and answers it, so the 407 comes back as an ordinary response. For HTTPS the client first sends CONNECT host:443 to open a raw tunnel; TLS and the real request happen inside it, invisible to the proxy. A 407 rejects the CONNECT itself, so the tunnel never opens and no HTTP response object is ever built. The same failure shows up under a different name in every client:
- Python requests:
ProxyError: Unable to connect to proxy … Tunnel connection failed: 407 Proxy Authentication Required - httpx:
httpx.ProxyError: 407 Proxy Authentication Required - curl:
curl: (56) CONNECT tunnel failed, response 407 - Node (undici / fetch):
TypeError: fetch failed … cause: Proxy response (407) - Chrome:
ERR_TUNNEL_CONNECTION_FAILEDor a proxy sign-in dialog - Scrapy:
twisted.web._newclient.ResponseNeverReceivedorTunnelError: Could not open CONNECT tunnel … 407
So the first thing to do is reproduce against an http:// URL. Hit http://httpbin.org/ip through the proxy and the 407 arrives as a readable response with its challenge header, which tells you the scheme and confirms the proxy is the one talking.
The five causes, in order of likelihood
1. The credentials never left your client
The most common cause is a configuration that looks right and sends nothing. In Python requests the proxies dict is keyed by the target scheme: a dict with only an "http" key sends HTTPS requests straight to the site, and a dict with a bare "https" key pointing at an unauthenticated URL sends the CONNECT without credentials. Environment variables are the other trap: HTTPS_PROXY set in the shell without a user and password overrides nothing you wrote in code, but a NO_PROXY entry that matches your host silently bypasses the proxy. Playwright and Puppeteer have a separate username/password field in the proxy object; putting them in the server URL is ignored by Chromium.
2. Wrong credentials
Proxy credentials are not your dashboard login. Most gateways generate a dedicated proxy user per plan, and the username often carries targeting flags. On QuanticData the username is USER-country-us style: the base user plus the country, and a session token if you want a sticky IP. A typo inside the flags, a country code the plan does not include, or a copy-paste that grabbed a trailing space all produce a 407 that looks identical to a wrong password.
3. A special character that was not URL-encoded
If the password contains @, :, /, # or % and you put it in a URL like http://user:pa@ss@host:7777, the client splits the URL at the first @ and sends garbage. Percent-encode it (@ becomes %40) or, better, pass the credentials through the client's dedicated option instead of the URL. In curl that is -U user:pass, which never touches URL parsing.
4. IP allowlist plans and an egress IP that changed
Some plans authenticate by source IP instead of user and password. On QuanticData that is the case for Residential Basic, Datacenter, ISP, IPv6 and Mobile; Residential Premium uses user:pass. With an allowlist plan, a 407 means the request came from an IP you did not register: a new cloud instance, a home connection whose address rotated overnight, or a container egressing through a NAT you did not know about. Fetch your public IP without the proxy, compare it with the allowlist, and update it. The allowlist is editable via POST /v1/public/proxies/whitelist-ip as documented in the API reference, so you can automate it at boot.
5. The plan itself
Gateways answer 407 for a suspended account, an expired plan, a plan whose bandwidth reached zero, or a concurrency cap on cheaper tiers. If the credentials worked yesterday and nothing on your side changed, check the plan balance before touching code. The same credentials against the wrong port, for example a SOCKS5 port with an HTTP client, produce a connection error rather than a 407, which is a useful way to tell the two apart.
Fix it in curl, Python, Node and Scrapy
Every example uses the residential gateway format from the residential proxies page: pr.quanticdata.io:7777 with a username that carries the country flag.
# curl: -U keeps credentials out of URL parsing; -v shows the CONNECT exchange
curl -v -x pr.quanticdata.io:7777 -U "USER-country-us:PASS" https://ipinfo.io/json
# Python requests: same proxy under BOTH keys, credentials inside the URL
from urllib.parse import quote
proxy = f"http://USER-country-us:{quote('PASS', safe='')}@pr.quanticdata.io:7777"
r = requests.get("https://ipinfo.io/json", proxies={"http": proxy, "https": proxy}, timeout=30)
# httpx: one proxy string for every scheme
with httpx.Client(proxy="http://USER-country-us:PASS@pr.quanticdata.io:7777") as c:
print(c.get("https://ipinfo.io/json").json())
// Node 18+: undici ProxyAgent takes a token so no URL encoding is needed
import { ProxyAgent } from "undici";
const dispatcher = new ProxyAgent({
uri: "http://pr.quanticdata.io:7777",
token: "Basic " + Buffer.from("USER-country-us:PASS").toString("base64"),
});
const r = await fetch("https://ipinfo.io/json", { dispatcher });
# Scrapy: per-request meta, HttpProxyMiddleware adds Proxy-Authorization itself
yield scrapy.Request(url, meta={"proxy": "http://USER-country-us:PASS@pr.quanticdata.io:7777"})
Two rules cover most of the rest. Catch ProxyError (requests, httpx) or exit code 56 (curl) as a 407, not as a network failure, so your retry logic does not treat it as transient and hammer the gateway. And never fall back to the direct connection on a proxy error: that leaks your real IP to the site you were proxying.
A 60-second diagnosis
- Reproduce on plain HTTP.
curl -v -x host:port http://httpbin.org/ipwithout credentials. You should seeProxy-Authenticate: Basic realm=…. No header means it is not a real proxy challenge. - Add
-U. If it now works in curl but not in your code, the credentials are not leaving your client (cause 1) or are being mangled by the URL (cause 3). - Check the egress IP.
curl https://ipinfo.io/ipwithout the proxy. On an allowlist plan it must match the dashboard. - Check the username flags. Drop everything after the base user and retry. If that works, a targeting flag is wrong or not included in the plan.
- Check the plan. Bandwidth left, expiry, concurrency. A gateway that says 407 for “no balance” is common.
What a 407 is not
It is not a block. A ban, a rate limit or an anti-bot challenge comes from the target and arrives as 403, 429, a CAPTCHA page or a 200 with the wrong body. Those are the errors where proxy quality, headers and pacing matter; a guide to the whole family of failures is in proxy not working: a 10-step checklist. A 407 means the request stopped one hop earlier, at a machine you pay for, and it will keep stopping there until the credentials or the allowlist are right. If you would rather not manage gateway credentials at all, the web scraping API runs the proxy layer for you and bills per successful page, so a request that fails at any hop is never charged.