407 Proxy Authentication Required: How to Fix It

HTTP 407 means the proxy, not the website, wants credentials. See the causes and tested fixes for curl, requests, httpx, Node, Playwright and Puppeteer.

Written byAndrii
Published on
407 Proxy Authentication Required: How to Fix It

An HTTP 407 comes from the proxy, not from the website. The proxy is asking for the credentials you were issued, so the one-line fix is: send a valid Proxy-Authorization header (in practice, user and password in the proxy setting, with special characters percent-encoded), and check that the proxy you are hitting is the one you meant. Repeating the same request will not help. Jump to the per-client fixes if you already know the cause.

Last updated: October 25, 2026. I tested every fix against a proxy and an origin I wrote, both bound to 127.0.0.1 (curl 8.7.1, Python 3.11.2, requests 2.32.3, httpx 0.27.2, Node v26.12.1 with undici 7.25.0, Playwright 1.64.1, Puppeteer 25.14.0, macOS). The proxy simulates common behavior and is not any real vendor or corporate proxy. I did not run Digest, NTLM or Kerberos handshakes. Credentials in this post are placeholders.

A small laptop window on the left is joined by a thin line to a green toll booth with an amber key tag, and a pale blue cloud server stands further right

What the 407 Status Code Means

RFC 9110 says the 407 status code "is similar to 401 (Unauthorized), but it indicates that the client needs to authenticate itself in order to use a proxy for this request." The proxy must send a Proxy-Authenticate header with a challenge, and the client "MAY repeat the request with a new or replaced Proxy-Authorization header field" (RFC 9110, section 15.5.8, read October 25, 2026). MDN says the same in simpler words. An authentication proxy is just a proxy that refuses to forward anything until the client proves who it is.

The rule I use to tell it from a 401: look at the header name. Proxy-* belongs to the proxy, WWW-Authenticate belongs to the website.

StatusIssued byChallenge headerClient answers withTypical fix
401 UnauthorizedThe website (origin server)WWW-AuthenticateAuthorizationSend valid credentials for the site
407 Proxy Authentication RequiredThe proxyProxy-AuthenticateProxy-AuthorizationSend valid credentials for the proxy
403 ForbiddenThe websiteNone requiredCredentials alone usually do not change itSee 403 Forbidden

I checked the first two rows on my local servers with curl. Same client, no credentials:

curl -sS -i -x http://127.0.0.1:PROXYPORT http://origin.test:ORIGINPORT/ok
curl -sS -i http://origin.test:ORIGINPORT/secret   # origin reached directly
HTTP/1.0 407 Proxy Authentication Required
Proxy-Authenticate: Basic realm="local-proxy"

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="origin"

Adding Authorization to the proxy request still gave a 407, and adding Proxy-Authorization to the origin request still gave a 401 (I tested both). When I sent valid proxy credentials to the /secret route, the proxy let the request through and the origin answered its own 401. A request can meet both challenges, one after the other.

To see what your client really sends, read the outgoing header with curl -v and decode it:

> Proxy-Authorization: Basic dXNlcjpwQHNzOndvcmQ=
decoded: user:p@ss:word

Base64 is encoding, not encryption. MDN notes that Base64-encoded Basic credentials "can be considered equivalent to sending the credentials in clear text" (MDN, Proxy-Authorization). Decoding the header is the fastest way to find a wrong password, a stray space or a double-encoded value.

Why You Get a 407: Common Causes

Most causes are in the table. The right column says how I know, or that I did not test it.

CauseSymptomCheck
No credentials sent407 with Proxy-AuthenticateProxy log or curl -v shows no Proxy-Authorization (I tested: 407 in all six clients)
Wrong user or password407 again, after a retryDecode the header and compare (tested)
Special characters not encodedInvalid URL, or the wrong password is sentPercent-encode (see below, tested)
Variable unset or empty in CI or Docker407, or no proxy used at allPrint the variable inside the process (tested)
HTTPS target, 407 on CONNECTLooks like a network errorRead the full error text (tested)
Proxy offers Digest, NTLM or Negotiate, client sends Basic407 after you sent credentialsRead Proxy-Authenticate (Digest header tested, handshake not run)
Wrong proxy host or portA different proxy answersPrint the effective proxy setting (not run)
Plan expired or credentials bound to another IP407 with correct credentialsAsk the vendor (not run)

When the HTTP call itself is an HTTPS request, the 407 happens on the CONNECT request, before TLS starts, so the client never sees the website. The client first asks the proxy for a tunnel, and that request is the one refused. The symptom misleads: requests raised ProxyError ... Tunnel connection failed: 407 Proxy Authentication Required, curl exited with curl: (56) CONNECT tunnel failed, response 407, and Playwright timed out (I set a 12 second limit). None of those say "wrong password" first.

Works on my laptop, 407 in Docker or CI

This is the most common version in crawl jobs. A short checklist:

  • The proxy variable is set on your laptop but not passed into the container or worker. Print it inside the process.
  • The variable exists but one part is empty: http://:@host:port produced a 407 in requests, httpx and Node.
  • Internal hosts go through the proxy because NO_PROXY is missing. The reverse also happens: NO_PROXY matched, the proxy was skipped, and the host did not resolve (tested).
  • IT rotated the credentials, or the proxy sits behind single sign-on (NTLM or Kerberos) and your script has no way to answer.
  • The system proxy setting and the application setting differ.

Ask the proxy owner for credentials or an exception. A 407 is the proxy doing its job, so I do not treat it as something to work around.

A flow chart titled Got a 407: what next. It checks which header came back, which proxy answered, the scheme, whether credentials are sent, special characters, environment variables and HTTPS, and ends with asking the proxy owner

Special Characters and Environment Variables

A proxy URL has the shape scheme://user:password@host:port. If the password contains @, :, /, # or ?, the parser has to guess where the password ends. I used two placeholder passwords on the local proxy, p@ss:word and p#ss/w?rd, once raw and once percent-encoded in the URL:

Password in the proxy URLcurlrequestshttpxNode fetch (undici)
p@ss:word rawcurl: (5) Unsupported proxy syntax200200200
p#ss/w?rd rawcurl: (5) Unsupported proxy syntaxInvalidURLInvalidURLInvalid URL
Both, percent-encoded200200200200

The first raw password worked in three clients, but curl refused it. The second never reached the proxy in any of the four. Do not rely on raw characters: encode them.

from urllib.parse import quote

password = quote("p@ss:word", safe="")   # p%40ss%3Aword
proxy = f"http://user:{password}@proxy.example.com:8000"

In Node, encodeURIComponent("p@ss:word") gives the same p%40ss%3Aword. Two exceptions: curl's -U user:password option takes the raw value, and Playwright and Puppeteer take user and password as separate fields, where the raw value is correct and an encoded one is wrong (more below).

Which clients read proxy environment variables?

Environment variables are the usual way a proxy gets into a worker. I ran each client with only the listed variables set, against a local proxy that required credentials:

SetupcurlrequestshttpxNode, NODE_USE_ENV_PROXY=1Node, no flag
HTTP_PROXY (upper), http targetnot usedproxiedproxiedproxiednot used
http_proxy (lower), http targetproxiedproxiedproxiedproxiednot used
Only HTTPS_PROXY, http targetnot usednot usednot usednot usednot used
HTTPS_PROXY or https_proxy, https targetproxiedproxiedproxiedproxiednot used
HTTP_PROXY with empty user:passnot used407407throws (407)not used
NO_PROXY matching the hostskippedskippedskippedskippedn/a

"Not used" means the proxy log showed no request and the host failed to resolve. On macOS, curl read the lowercase http_proxy and ignored the uppercase HTTP_PROXY, which matches the curl man page: "http_proxy is an exception as it is only available in lower case" (curl man page, read October 25, 2026). The empty-credentials row has no curl result because curl never used the uppercase variable.

Node's built-in fetch ignored all of them until I set NODE_USE_ENV_PROXY=1. Per the Node docs that support was added in v24.5.0 and v22.21.0 and is marked "Stability: 1.1 - Active development" (Node.js docs, built-in proxy support). I ran Node v26.12.1 and did not test Node 18, 22 or 24. On older versions, use undici's ProxyAgent (next section).

Basic, Digest and NTLM: Check the Scheme

The Proxy-Authenticate header names the scheme. Basic is a Base64 user and password. Digest, NTLM and Negotiate (Kerberos) are other schemes, common on corporate proxies tied to single sign-on. I pointed a second local proxy at the clients and made it answer Proxy-Authenticate: Digest and never accept anything. That is header text only: I did not implement a Digest, NTLM or Kerberos handshake, so I cannot tell you a fix works for them.

What the run showed: curl, requests, httpx and fetch sent Basic credentials once and got a 407 back. Playwright and Puppeteer (Chromium) sent a second request with a Digest authorization header, so Chromium answered the challenge on its own. My test proxy never accepts it, so that is not a working Digest test. For the other clients, the plain options are a Basic-capable service account from the proxy owner, or a local forwarding proxy that speaks the scheme for your script.

Fixing 407 in curl, Python, Node and Playwright

For each client: the failing call, then the fixed call. proxy.example.com:8000 stands for my local proxy, and the full setup for each client is in proxies for web scraping. Replace the hosts with a proxy you are allowed to use.

curl

# Fails: 407, or "CONNECT tunnel failed, response 407" for https:// targets
curl -sS -i -x http://proxy.example.com:8000 https://example.com/

# Fixed: -U takes the raw password
curl -sS -x http://proxy.example.com:8000 -U 'user:p@ss:word' https://example.com/

With -U, curl sent one request with Basic credentials. Adding --proxy-anyauth made it send the first request without credentials and retry after the 407: my proxy log showed two requests instead of one. The man page says it "might cause an extra request/response round-trip". Use it when you do not know the scheme.

Python requests and httpx

import requests
from urllib.parse import quote

pw = quote("p@ss:word", safe="")
proxy = f"http://user:{pw}@proxy.example.com:8000"

r = requests.get("https://example.com/", proxies={"http": proxy, "https": proxy}, timeout=15)
print(r.status_code)

Without credentials, requests returned the 407 as a normal response over plain HTTP. For an HTTPS target it raised the ProxyError ... Tunnel connection failed: 407 seen above. httpx raised ProxyError: 407 Proxy Authentication Required on HTTPS. In httpx 0.27.2 the argument is httpx.Client(proxy=proxy), and older versions used proxies=.

Node fetch (undici)

import { ProxyAgent } from "undici";

const pw = encodeURIComponent("p@ss:word");
const dispatcher = new ProxyAgent(`http://user:${pw}@proxy.example.com:8000`);

// Without credentials this throws instead of returning a 407 response
const res = await fetch("https://example.com/", { dispatcher });
console.log(res.status);

Node's fetch did not return a 407 response. It threw TypeError: fetch failed, and the real reason sat two levels down in cause: Proxy response (407) !== 200 when HTTP Tunneling. Even for an http:// target, ProxyAgent opened a CONNECT tunnel (my proxy log shows CONNECT), so a 407 always looks like this. Print err.cause and its cause when you debug.

Playwright

import { chromium } from "playwright";

const browser = await chromium.launch({
  proxy: { server: "http://proxy.example.com:8000", username: "user", password: "p@ss:word" },
});
const page = await browser.newPage();
const res = await page.goto("https://example.com/");
console.log(res.status());

Credentials go in username and password, raw, as the Playwright docs show. Without them, page.goto resolved with status 407 for an HTTP target and timed out for an HTTPS one. Putting the encoded credentials in the server URL did not work (407), and neither did passing the encoded value as the password (407, my proxy log showed p%40ss%3Aword arriving). Chromium skips the proxy for loopback by default, so test with a non-loopback hostname.

Puppeteer

import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://proxy.example.com:8000"],
});
const page = await browser.newPage();
await page.authenticate({ username: "user", password: "p@ss:word" }); // before goto
const res = await page.goto("https://example.com/");
console.log(res.status());

Without page.authenticate, goto failed with net::ERR_INVALID_AUTH_CREDENTIALS, and credentials in the --proxy-server URL gave net::ERR_NO_SUPPORTED_PROXIES. A wrong password produced status 407 and Chrome's own error page. The Puppeteer docs note that request interception is switched on behind the scenes to do this.

The client matrix

The table is my full run. Each row is a case, each cell is the status the client reported or how it failed. In rows 3 and 4 the browser columns got the raw value in the password field, not in a URL.

A terminal table of eleven proxy authentication cases run with curl, requests, httpx, Node fetch, Playwright and Puppeteer, showing statuses 407 and 200 and error names

Two rows to remember. Row 7 shows the same HTTPS failure worded differently in every client. Row 8 shows that once the credentials are right, all six reach the origin through the tunnel.

Why Scrapers and Crawlers Hit 407

For a crawler the causes are the ones above, only more often: vendor credentials copied with a trailing space or an unencoded character, a session-style username typed in the wrong format, the variable set on a laptop but not in the worker, and a crawl that runs behind a corporate proxy. A tunnel 407 also hides behind network-error wording, so logs rarely say 407.

A retry does not fix it. Same credentials, same answer, and the RFC only says the client may repeat the request with a new or replaced header. If your crawl code retries every error, give a 407 a stop condition and alert someone. A 403 from the website is a different problem: see 403 Forbidden. For proxy types, pricing and the full setup, see proxies for web scraping.

I run WebCrawlerAPI, so this is the vendor view. The web scraping API page says the page "is rendered in a browser on our side, with retries, proxies and anti-bot handling, then converted and returned in the same response", and results differ by target site. The error docs list no 407-specific code, so I do not claim how a proxy problem would show up there. Test it on your own URLs.

Once the proxy accepts your credentials, residential vs datacenter proxies helps you pick the IP type, and exponential backoff and retries for HTTP clients covers what to do when requests still fail.

FAQ

What does the proxy status code 407 mean?

The proxy needs the client to authenticate before it forwards the request. It sends a Proxy-Authenticate header with the scheme, and the client can repeat the request with a Proxy-Authorization header.

What is an authentication proxy?

A proxy that only forwards requests from clients that send valid credentials. Corporate proxies and paid proxy services work this way.

Why do I keep getting a proxy authentication error?

Usually one of five things: no credentials, a wrong password, an unencoded special character, an empty variable in CI or Docker, or a scheme (Digest, NTLM) your client does not send. Decode the Proxy-Authorization header and read Proxy-Authenticate.

How do I fix error 407?

Send the right credentials to the right proxy, percent-encode them in a URL, and do not retry unchanged. If it still fails, ask the proxy owner.

What is the difference between 407 and 401?

A 401 comes from the website and asks for Authorization. A 407 comes from the proxy and asks for Proxy-Authorization.

Look at the header name first, fix the credentials or the scheme, and never retry the same request with the same credentials.


About the Author

Andrii Mazurian
Andrew Mazurian@andriixzvf

Founder, WebCrawlerAPI · 🇳🇱 Netherlands

Engineer with 15 years of experience in APIs, big data, and infrastructure. Founded WebCrawlerAPI in 2024 with a single goal: to build the best data API, and have been shipping it every day since.