Skip to content
Control Plane Labs

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/items

What 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