407 Proxy Authentication Required
Client error response defined by RFC 9110 §15.5.8.
Last updated July 27, 2026
Common causes at a glance
- A corporate egress proxy demanding NTLM, Negotiate, or Basic credentials
- HTTPS_PROXY set without embedded credentials in a container or CI runner
- Credentials rotated in the OS keychain but stale in an application config
- A client that only handles Basic against a proxy that offers Negotiate
Reproduce it
curl -i -x http://proxy.corp.example:3128 https://api.example.com/v1/itemsWhat 407 Proxy Authentication Required means
407 is almost exclusively a corporate-network phenomenon. An outbound proxy sits between the workstation or CI runner and the internet, requires authentication, and rejects anything it cannot attribute to a user. The origin server is never contacted.
The header pair is deliberately separate from the origin’s. Authorization is
for the target resource and passes through the proxy untouched;
Proxy-Authorization is consumed by the proxy and must not be forwarded. Both
can appear on one request, and confusing them is the usual reason a programmatic fix does
not work: setting Authorization to the proxy credential authenticates nothing
and leaks the password to the origin.
What the spec says
RFC 9110 §15.5.8 defines 407 as analogous to 401 but
for the proxy, and requires that the proxy MUST send a
Proxy-Authenticate header containing a challenge applicable to it. That field
is §11.7.1, the client’s response field
Proxy-Authorization is §11.7.2, and the shared
authentication framework is §11. These are hop-by-hop credentials:
§7.6.1 covers the connection-level fields an intermediary must not
blindly forward. For HTTPS the proxy sees only a CONNECT request
(§9.3.6), so a 407 on a TLS URL arrives before the tunnel exists,
which is why many clients surface it as a connection error rather than a status.
What actually causes it
Container and CI environments are where this bites, because the
workstation’s transparent proxy integration does not exist inside them. A developer’s
browser authenticates via Kerberos without anyone noticing, then the same request from a
Docker build gets 407 because HTTPS_PROXY carries a host and port but no
credentials. Scheme mismatch is second: Windows proxies commonly offer
Negotiate and NTLM, and a client implementing only
Basic has nothing to answer with, producing a 407 loop. Tooling that ignores
proxy environment variables fails differently again, usually as a connection timeout,
because the request never reaches the proxy.
How to debug it
Read the challenge first, because it tells you which scheme the proxy will accept and therefore whether your client can answer it:
curl -sv -x http://proxy.corp.example:3128 https://api.example.com/ 2>&1 \
| grep -i 'proxy-authenticate'
If it says Negotiate or NTLM and your client supports only
Basic, no amount of correct credentials will help; curl needs
--proxy-negotiate or --proxy-ntlm and a build that supports it.
Then verify the proxy is used at all. Print the environment the process actually sees —
HTTP_PROXY, HTTPS_PROXY, and crucially NO_PROXY,
since an over-broad NO_PROXY silently bypasses the proxy and an under-broad
one routes internal traffic through it. Test with the credential inline to separate a
credential problem from a configuration one, then move it to the keychain rather than
leaving it in shell history or a Dockerfile.
Working examples
curl
# What scheme is the proxy demanding?
curl -sv -x http://proxy.corp.example:3128 https://api.example.com/ 2>&1 \
| grep -i 'proxy-authenticate'
# Basic credentials inline, to isolate credential vs configuration.
curl -i -x http://user:pass@proxy.corp.example:3128 https://api.example.com/v1/items
# Windows-style proxies usually want one of these instead.
curl -i --proxy-ntlm -U 'DOMAIN\user:pass' \
-x http://proxy.corp.example:3128 https://api.example.com/v1/items
Python (requests)
import os, requests
# requests reads HTTP_PROXY/HTTPS_PROXY/NO_PROXY from the environment.
print({k: v for k, v in os.environ.items() if "PROXY" in k.upper()})
proxies = {"https": "http://user:pass@proxy.corp.example:3128"}
resp = requests.get("https://api.example.com/v1/items", proxies=proxies, timeout=15)
print(resp.status_code)
# Without credentials you get the challenge back; read the scheme it offers.
bare = requests.get(
"https://api.example.com/v1/items",
proxies={"https": "http://proxy.corp.example:3128"},
timeout=15,
)
print(bare.status_code, bare.headers.get("Proxy-Authenticate"))
Node (fetch)
// Node's fetch ignores HTTPS_PROXY unless you give it a dispatcher.
import { ProxyAgent, setGlobalDispatcher } from "undici";
const token = "Basic " + Buffer.from("user:pass").toString("base64");
setGlobalDispatcher(
new ProxyAgent({ uri: "http://proxy.corp.example:3128", token }),
);
const res = await fetch("https://api.example.com/v1/items");
console.log(res.status, res.headers.get("proxy-authenticate"));Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request