Skip to content
Proxy fundamentals

HTTP_PROXY, HTTPS_PROXY and NO_PROXY explained

The four proxy environment variables, tested across curl, wget, Python, Go and Node, including the case rule that silently ignores HTTP_PROXY.

Setting http_proxy looks like the simplest way to put a whole machine behind a proxy. It is also the configuration that most often behaves differently from how you read it, because the variables are a convention rather than a standard. No RFC defines them. Every client implemented its own reading of the same four names, and the readings disagree.

This guide records what four widely used clients actually do, tested rather than quoted. Where they diverge, the divergence is the whole point: a rule that holds in Python quietly fails in Go, and the failure is silent traffic going somewhere you did not intend.

#The four variables

Variable Applies to Notes
http_proxy Requests to http:// URLs The one variable with a case rule; see below
https_proxy Requests to https:// URLs Sets the proxy used for the CONNECT tunnel
all_proxy Any scheme with no more specific variable Not universal; curl honours it, Go does not
no_proxy Exemptions from all of the above Comma-separated; matching rules differ per client

The value takes the same form as the -x argument to curl: scheme://host:port, with optional inline credentials. A username and password placed here is visible to every process that can read the environment, which is a reason to prefer per-client configuration for anything sensitive.

# The conventional form
export http_proxy="http://gateway.example:8000"
export https_proxy="http://gateway.example:8000"
export no_proxy="localhost,127.0.0.1,.internal.example"

#Case matters, and not in the direction you expect

The instinct is that the uppercase and lowercase spellings are interchangeable. For three of the four variables they usually are. For http_proxy they are not, and the exception exists for a security reason.

In a CGI environment, every inbound HTTP request header is exposed to the script as an environment variable with an HTTP_ prefix. A request carrying a Proxy: header therefore arrives as HTTP_PROXY. If the HTTP client inside that script honoured it, any visitor could redirect the server’s outbound traffic to a host of their choosing. This was disclosed in 2016 as httpoxy, and the fix in most clients was to stop trusting the uppercase spelling of that one name.

The result is that the safest-looking spelling is the one most likely to be ignored. Tested on curl 8.7.1, GNU wget, CPython 3.9 and Go 1.24, each against a proxy address that refuses connections, so that “the request succeeded” proves the proxy was not used:

Client http_proxy (lowercase) HTTP_PROXY (uppercase)
curl 8.7.1 Honoured Ignored
GNU wget Honoured Ignored
Python 3.9 urllib / requests Honoured Honoured, unless REQUEST_METHOD is set
Go 1.24 net/http Honoured Honoured

The Python row is the subtle one. CPython treats the presence of REQUEST_METHOD in the environment as evidence that it is running under CGI, and drops uppercase HTTP_PROXY only in that case. The same script behaves differently depending on a variable that has nothing to do with proxying:

$ HTTP_PROXY=http://127.0.0.1:9 python3 -c \
    "import urllib.request as u; print(u.getproxies_environment())"
{'http': 'http://127.0.0.1:9'}

$ REQUEST_METHOD=GET HTTP_PROXY=http://127.0.0.1:9 python3 -c \
    "import urllib.request as u; print(u.getproxies_environment())"
{}

The practical rule: always set the lowercase spelling. Every client tested honours it, and it is the only spelling with no special case attached. Setting both costs nothing and removes the question entirely.

#no_proxy: four clients, four sets of rules

The exemption list is where the real divergence lives. The same no_proxy string produces different routing in different runtimes, and nothing warns you.

Each row below was tested against a proxy address that refuses connections. “Bypass” means the request reached the destination directly. “Proxied” means the client tried the proxy, so the exemption did not apply. A control with an empty no_proxy confirmed the test detects proxy use.

no_proxy value Requested host curl 8.7.1 Python 3.9 Go 1.24
.example.org api.example.org Bypass Bypass Bypass
.example.org example.org Bypass Bypass Proxied
example.org api.example.org Bypass Bypass Bypass
10.0.0.0/8 10.1.2.3 Bypass Proxied Bypass
* Any Bypass Bypass Bypass

Two rows deserve attention.

The leading dot. Go reads .example.org as “subdomains of example.org, and not the apex itself”. curl and Python read it as “example.org and anything below it”. A team that writes no_proxy=".internal.corp" and expects internal.corp to be exempt gets that behaviour in Python and the opposite in Go. Write the apex explicitly — internal.corp,.internal.corp — and the ambiguity disappears.

CIDR ranges. curl and Go both match an address against a network in no_proxy. CPython’s urllib does not; it compares strings, so 10.0.0.0/8 never matches 10.1.2.3 and the request goes to the proxy. If a Python service must reach an internal network directly, list the addresses or hostnames rather than the range.

#Which clients read the environment at all

An assumption worth checking before you debug anything else: not every client looks at these variables. Setting them and seeing no change usually means the client never read them, not that the value is wrong.

Client Reads proxy environment by default
curl, wget Yes
Python requests, urllib Yes; disable with session.trust_env = False
Go net/http default transport Yes
Node.js global fetch (tested on 24.2.0) No
Browsers No; they use system or PAC configuration

Node’s built-in fetch is the common surprise. On Node 24.2.0 a request made with HTTPS_PROXY set reached the destination directly. Node needs an explicit dispatcher, typically ProxyAgent from undici, or an agent-aware client such as axios or got. Setting the variable and assuming it applied is how traffic you believed was proxied leaves from your own address.

#Proving which variable actually applied

Do not infer this. Ask the destination to report the address it saw, then compare against the same request with the environment cleared:

# With whatever the environment says
curl -s https://api.ipify.org

# With every proxy variable removed, as a control
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \
    -u all_proxy -u ALL_PROXY -u no_proxy -u NO_PROXY \
    curl -s https://api.ipify.org

If the two agree, no proxy was applied, whatever the variables say. curl will also tell you directly:

curl -v https://example.com/ 2>&1 | grep -i 'proxy\|Connected to'

A line reading Connected to gateway.example rather than the destination host confirms the tunnel. On a rotating endpoint the reported address describes one exit node at one moment, not the pool as a whole.

#The three places the variables do not reach

Most reports of “I set the proxy and nothing happened” trace to one of three boundaries the environment does not cross.

A container does not inherit the host shell. docker run starts a process with its own environment. Variables exported in your terminal are not passed unless you say so, and a build is a separate case again, because docker build runs each step in its own container:

# Runtime
docker run -e http_proxy -e https_proxy -e no_proxy myimage

# Build time, and only for ARGs the Dockerfile declares
docker build --build-arg http_proxy --build-arg https_proxy .

Pulling the image itself is a third case. That is the Docker daemon’s own outbound request, not your shell’s, so it reads the daemon configuration rather than anything you exported.

A service manager does not inherit your login shell. A unit started by systemd gets the environment systemd gives it. Exporting a variable in .bashrc and restarting the service changes nothing; the setting belongs in the unit, usually as an Environment= line or an EnvironmentFile. The same applies to anything started by cron, which runs with a deliberately minimal environment.

Windows uses a different mechanism entirely. The lowercase convention comes from Unix. Native Windows HTTP stacks read the WinHTTP or WinINET configuration instead, so a tool built on those ignores the variables even when they are set. Cross-platform tools that carry their own HTTP client — curl, Python, Go — still read them, and PowerShell sets them with a different syntax:

$env:http_proxy  = "http://gateway.example:8000"
$env:https_proxy = "http://gateway.example:8000"

The pattern behind all three: the variables configure a process, and they only apply to processes that inherited them from a parent that had them. Whenever the request is made by something you did not start from that shell, they do not apply.

#When environment variables are the wrong tool

They are process-wide and inherited. That is convenient for a one-off shell session and a liability everywhere else.

  • Credentials leak into process listings and logs. Anything that dumps the environment on error — a crash reporter, a debug endpoint, a CI log — dumps the password with it.
  • Everything inherits them. A package manager, a telemetry agent and a health check all start using the proxy, and on metered bandwidth that is billable traffic you did not intend to send.
  • They cannot express per-request routing. Work that needs a different session or country per request needs client-level configuration.

Use the environment for interactive debugging and for tools that offer no other configuration. For anything running unattended, configure the client directly, so the setting is visible in the code that depends on it rather than in the shell that happened to start it.

#How this was tested

Every result above came from running the command, not from documentation. Each proxy variable pointed at http://127.0.0.1:9, a port that refuses connections, so a successful request proves the proxy was skipped and a failure proves it was attempted. A control run with the exemption list empty confirmed the method detects proxy use in each client. Results were stable across repeated runs.

Versions: curl 8.7.1, GNU wget, CPython 3.9 with requests 2.32.3, Go 1.24.4, Node.js 24.2.0. Behaviour changes between versions, so re-run the check on your own runtime before depending on any row.

Frequently asked questions

Why does curl ignore HTTP_PROXY but honour http_proxy?
Because in a CGI environment every inbound request header is exposed as an environment variable with an HTTP_ prefix, so a visitor sending a Proxy header creates HTTP_PROXY. Honouring it would let any visitor redirect the server's outbound traffic. This was disclosed in 2016 as httpoxy, and curl now trusts only the lowercase spelling of that one name. The other variables have no such collision and both cases work.
Should I set both the uppercase and lowercase versions?
Set the lowercase spelling first, because every client tested honours it. Setting the uppercase versions of https_proxy and no_proxy as well is harmless and helps tools that only check that case. The one you should not rely on alone is uppercase HTTP_PROXY.
Why is my no_proxy entry ignored in one language and not another?
The variable has no specification, so each client implemented its own matching. Tested on curl 8.7.1, CPython 3.9 and Go 1.24, a leading dot such as .example.org exempts the apex domain in curl and Python but not in Go, and a CIDR range works in curl and Go but not in Python. Write both the apex and the dotted form, and list addresses rather than ranges when Python is involved.
I set https_proxy in Node and requests still go out directly. Why?
Node's global fetch does not read proxy environment variables. This was confirmed on Node 24.2.0: a request with HTTPS_PROXY set reached the destination unproxied. Node needs an explicit dispatcher, usually ProxyAgent from undici, or a client such as axios or got configured with an agent.
Why does my systemd service ignore the proxy I exported?
A service does not inherit your login shell. systemd supplies the unit's environment itself, so the setting belongs in the unit file as an Environment= line or an EnvironmentFile, not in .bashrc. Cron behaves the same way, running with a deliberately minimal environment.

Sources

  1. curl manual page: environment variables and proxy options
  2. httpoxy: the CGI namespace collision behind the HTTP_PROXY exception
  3. Python urllib.request: getproxies and proxy_bypass
  4. Go httpproxy: NO_PROXY matching, including CIDR

Read this page as Markdown · Quote it freely under CC BY 4.0 with a link back.