Troubleshooting

The no_proxy environment variable

How curl, Go, Python requests, urllib, wget and Java each interpret no_proxy, the portable subset that works everywhere, and the suffix-matching trap.

· 14 min read · How we verify this

Key points

  • There is no specification for no_proxy. Every implementation invented its own matching rules, and they disagree on leading dots, wildcards, CIDR, ports and which case of the variable wins.
  • All six now require a label boundary, so example.com does not match badexample.com. The old plain-suffix behaviour survives only in older builds: Python requests before 2.34.0 and GNU wget 1.16 and earlier.
  • The portable subset is: comma separated, no spaces, bare hostnames and literal IPs only, no leading dots, no wildcards, and both no_proxy and NO_PROXY set to the same value.
  • Go never proxies localhost or a loopback address regardless of no_proxy; curl, requests, urllib on POSIX and wget all will proxy them unless you list them. Java exempts them through the default value of http.nonProxyHosts.

no_proxy is a list of destinations that should be reached directly instead of through the proxy named by http_proxy or https_proxy. There is no RFC, no draft and no shared test suite for it, so every HTTP client implemented its own parser and its own matching rule, and those rules genuinely disagree: on whether a leading dot matches the bare domain, on whether matching respects label boundaries, on wildcards, CIDR, ports, and on whether NO_PROXY or no_proxy wins when both are set. A value that works perfectly in Go can silently proxy half your traffic in Python.

The tables below model documented and source-derived behaviour for six widely deployed implementations. Check any specific value against your exact runtime with the no_proxy tester before relying on it, because these are implementation details rather than a contract, and they do change between releases. For the command-line side of the same problem, see curl through a proxy.

What the six implementations are#

ImplementationVersion this describesWhere the value comes from
curl7.86.0 and later--noproxy, no_proxy / NO_PROXY, .curlrc
Gogolang.org/x/net/http/httpproxy, which net/http's ProxyFromEnvironment usesNO_PROXY / no_proxy, or a httpproxy.Config struct
Python requests2.34.0 and laterno_proxy / NO_PROXY, proxies={"no_proxy": ...}, session.trust_env
CPython urlliburllib.request in current CPython 3.x*_proxy environment variables via getproxies() and proxy_bypass()
GNU wget1.20 and laterno_proxy environment variable, .wgetrc, --no-proxy
JavaJDK 8 and later-Dhttp.nonProxyHosts system property. The NO_PROXY environment variable is ignored entirely.

Parsing and precedence#

BehaviourcurlGorequestsurllibwgetJava
Separatorcommacommacommacommacommathe pipe character
Spaces around entriestoleratedtrimmedall spaces stripped from the whole valuetrimmednot toleratednot tolerated
Which case of the variable winslowercase no_proxy checked firstuppercase NO_PROXY checked first in every released Go toolchain to date and in x/net up to v0.57.0; x/net v0.58.0 switched to lowercase firstlowercase firstlowercase preferred (two-pass read)lowercase documentedn/a, it is a system property
Lone * disables the proxyyes, only when the whole value is exactly *yes, and also as one entry in a listnot by its own matcher; on POSIX it defers to urllib, which honours a value of exactly *yes, only when the whole value is exactly *no, * is not specialyes, * is a wildcard character, so a lone * matches everything
Ignores HTTP_PROXY under CGIyes, never reads uppercase HTTP_PROXYyes, errors if REQUEST_METHOD is setinherits urllib's behaviour on the fallback pathyes, drops the http proxy when REQUEST_METHOD is setnot applicablenot applicable
Default value when unsetnonenonenonenonenonelocalhost, 127.*, [::1], 0.0.0.0 and [::0] joined by pipes in the OpenJDK source; Oracle's networking-properties page documents only the first three

Two rows in that table cause real incidents. Go has historically checked the uppercase variable first while curl and Python check the lowercase one first, so a host that sets no_proxy=internal.example.com and NO_PROXY= (empty, or stale, or different) gets divergent behaviour from a Go binary and a shell script on the same machine. That divergence is being removed: golang.org/x/net v0.58.0 changed FromEnvironment to prefer the lowercase name, but the change had not reached a released Go toolchain at the time of writing, so treat the precedence as version-dependent rather than fixed. And Java does not read the environment variable at all, which is why a JVM in a container that has correct NO_PROXY set still sends internal traffic through the proxy.

Matching semantics#

Read this as: does the pattern in the left column match the host described?

Pattern and hostcurlGorequestsurllibwgetJava
example.com matches example.comyesyesyesyesyesyes
example.com matches api.example.comyesyesyesyesyesno
.example.com matches example.comyes, leading dot ignoredno, subdomains onlyyes, leading dot stripped (since 2.34.0; no before)yes, leading dots strippednono, and it matches nothing at all
.example.com matches api.example.comyesyesyesyesyesno
example.com matches badexample.comnonono (since 2.34.0; yes before)nono (since 1.20; yes in 1.16 and earlier)no
*.example.com matches api.example.comno, pattern unsupportedyes, normalised to .example.comnononoyes
*.example.com matches example.comnononononono
10.0.0.0/8 matches 10.1.2.3yes (IPv4 and IPv6, since 7.86.0)yes (IPv4 and IPv6)yes, IPv4 onlynonono
10.0.0.1 matches host 10.0.0.1yesyesyesyesyesyes
192.168.* matches 192.168.1.5nononononoyes
example.com:8443 matches that host and portno, ports are not comparedyesyes, both host and host:port are testedyes, host is compared with and without portnono
Matching is case insensitiveyesyeshost is lowercased, pattern is notyesyesyes
localhost and loopback exempt without being listednoyes, alwaysno on POSIXno on POSIX; macOS and Windows defer to system settingsnoyes, via the default property value

The second-order effect is that a "safe" entry in one client can still be a leak in another, because fleets are not uniformly patched. Adding example.com to a shared NO_PROXY in a base container image is correct for curl, Go and current Python, and is a data-exfiltration path for any image still pinned to requests below 2.34.0, which would talk to an attacker-registered notexample.com directly rather than through the logging and filtering proxy. Pin and check the client version, not just the value.

The portable subset#

If a value must work identically across every client in a fleet, restrict it to what all of them agree on:

  • Comma separated, no spaces. wget and Java tolerate no whitespace; requests strips all spaces including ones inside hostnames, which mangles nothing in practice but is not something to rely on.
  • Bare hostnames only. Write example.com, never .example.com. Go, wget and Java all fail to match the bare domain from a dotted entry, while curl, urllib and requests 2.34.0 and later treat the two as equivalent.
  • List the bare domain and accept that Java needs more. example.com covers subdomains everywhere except Java, so for JVM workloads add *.example.com alongside it in http.nonProxyHosts.
  • Literal IP addresses, not CIDR. urllib, wget and Java have no CIDR support. If you need a range on those clients you must enumerate addresses, or use Java's 10.* prefix wildcard, or fix it at a different layer.
  • No ports. curl and wget ignore port-qualified entries, so a host:port entry there matches nothing.
  • Always list localhost, 127.0.0.1 and ::1 explicitly. Only Go and Java exempt them by default.
  • Set no_proxy and NO_PROXY to the same value. This costs one line and removes an entire class of divergence, given that Go reads uppercase first and everything else reads lowercase first.

A value that satisfies all of that:

bash
export no_proxy="localhost,127.0.0.1,::1,example.com,internal.example.com,10.0.0.5"
export NO_PROXY="$no_proxy"
# JVM workloads additionally need, note the pipes and the explicit subdomain wildcard:
export JAVA_TOOL_OPTIONS="-Dhttp.nonProxyHosts=localhost|127.*|[::1]|example.com|*.example.com"

The loopback entries above are belt and braces. In the OpenJDK DefaultProxySelector, a non-empty http.nonProxyHosts value has the built-in loopback defaults appended to it rather than replacing them, so loopback stays exempt either way; setting the property to the empty string is what removes them. Repeating them costs nothing and does not depend on that behaviour holding in every JDK. Java's HTTPS handler also uses http.nonProxyHosts, so there is no separate https.nonProxyHosts to set.

Why NO_PROXY does not fix Kubernetes cluster-internal traffic#

Setting NO_PROXY=.svc.cluster.local in a pod and finding that internal calls still traverse the proxy is the most common no_proxy support question, and it has three separate causes stacked on top of each other.

Matching happens on the URL host string, before DNS. Every implementation here compares the literal host from the URL. In a cluster, application code calls http://payments/v1/charge or http://payments.billing/v1/charge, relying on the pod's search domains to complete the name. The client never sees payments.billing.svc.cluster.local, so a .svc.cluster.local entry matches nothing. You must list the short forms your code actually uses.

CIDR entries do not apply to names. A 10.96.0.0/12 entry is only consulted when the URL host is a literal IP address. Calls made by service name never reach the CIDR branch, no matter how correct the range is.

The Kubernetes API server is reached by IP. In-cluster clients read KUBERNETES_SERVICE_HOST, which is the ClusterIP (commonly 10.96.0.1), so the API server needs an IP or CIDR entry, not a name.

A working set for a cluster with service CIDR 10.96.0.0/12 and pod CIDR 10.244.0.0/16:

bash
NO_PROXY=localhost,127.0.0.1,::1,\
10.96.0.0/12,10.244.0.0/16,\
.svc,.svc.cluster.local,.cluster.local,\
kubernetes.default,kubernetes.default.svc,\
169.254.169.254,\
.internal.example.com

169.254.169.254 matters because a proxy in front of the cloud metadata endpoint breaks instance credentials and, worse, can turn a benign HTTP client into an SSRF vector against the proxy layer. The node CIDR should be added too if pods talk to kubelet or to node-local services.

The dotted entries .svc and .cluster.local are there for the clients that do use FQDNs and that handle leading dots, and they are harmless in the clients that do not. Given the divergence in the table above, the robust approach in a mixed-language cluster is to list both the dotted and the bare form of every internal suffix.

Docker and containerd#

Three separate proxy configurations exist and they do different things.

The Docker CLI's ~/.docker/config.json injects proxy variables into containers at docker run and into builds:

json
{
  "proxies": {
    "default": {
      "httpProxy": "http://proxy.example.com:3128",
      "httpsProxy": "http://proxy.example.com:3128",
      "noProxy": "localhost,127.0.0.1,::1,.internal.example.com,10.0.0.0/8"
    }
  }
}

The value is passed verbatim into the container environment as both cases, so whatever divergence exists between the clients inside the image applies unchanged. A CIDR entry here is useful to Go and curl processes in the container and inert to a JVM.

The Docker daemon's own environment, set with a systemd drop-in at /etc/systemd/system/docker.service.d/http-proxy.conf, governs the daemon's outbound traffic, which is image pulls and registry auth. It has no effect on containers. Registry hostnames belong here.

containerd is configured separately at /etc/systemd/system/containerd.service.d/http-proxy.conf with the same Environment="NO_PROXY=..." syntax. On a Kubernetes node, this is the one that decides whether image pulls from an internal registry go through the proxy, and it is routinely the config that gets missed after NO_PROXY is fixed everywhere else. Both daemons need systemctl daemon-reload and a restart to pick up changes.

Browsers do not use no_proxy at all#

Worth stating because it derails investigations: no_proxy is a convention among command-line tools and HTTP libraries. Browsers and most desktop applications take their bypass list from OS proxy settings or from a PAC file's FindProxyForURL logic, which has completely different semantics (shExpMatch, isInNet, dnsDomainIs) and is evaluated per request in JavaScript. A rule that works in the shell has no bearing on what Chrome does, and vice versa. See PAC files and WPAD, and test rules with the PAC file tester. The wider set of developer-tool proxy settings, each with its own configuration file, is covered in corporate proxies and developer tooling.

Failure modes#

Internal traffic still goes through the proxy after setting NO_PROXY. Check the process's actual environment (tr '\0' '\n' < /proc/PID/environ | grep -i proxy) rather than your shell's. Systemd units, container runtimes and CI runners each have separate environments.

Java ignores your NO_PROXY completely. Expected. The JDK reads http.proxyHost and http.nonProxyHosts system properties. Some libraries and frameworks add environment support on top, so behaviour differs between an HTTP client library and a plain HttpURLConnection in the same process.

.example.com matches subdomains but not the apex. In Go, wget and Java, and in requests before 2.34.0, .example.com does not match example.com. The apex is usually where the login endpoint lives, so the symptom is that everything works except authentication.

A CIDR entry that is silently ignored. urllib, wget and Java have no CIDR parser. wget will not error; it treats 10.0.0.0/8 as a literal suffix that matches almost nothing.

An uppercase entry that never matches in requests. requests lowercases the URL host but not the entries, so NO_PROXY=Internal.Example.COM never matches. Every other implementation here lowercases both sides.

no_proxy used as a security control. It is a client-side hint. A process can be started without it, a library may not implement it, and the suffix trap above widens it unpredictably. Egress policy belongs at the proxy, alongside the trusted proxy configuration that decides what the proxy will accept in the first place.

Frequently asked questions#

Is no_proxy a comma separated or space separated list?#

Comma separated in curl, Go, Python and wget. Java's http.nonProxyHosts uses the pipe character instead, and accepts no other separator. Do not use spaces after commas: wget and Java do not tolerate whitespace, and while curl, Go and urllib trim it, there is no benefit to relying on that.

Does no_proxy support wildcards like *.example.com?#

Only in Go and Java. Go normalises *.example.com to .example.com and matches subdomains only. Java supports a leading or trailing * in each pattern, and a pattern of *foo* matches anywhere in the host. curl, CPython's urllib, Python requests and wget do not support the pattern at all, though curl, Go, urllib and Java do treat a lone * as "bypass the proxy for everything". In curl and urllib that only works when the entire variable is *, not when * is one entry among several.

Should I write example.com or .example.com in no_proxy?#

Write the bare form, example.com. In curl, CPython's urllib and requests 2.34.0 and later the two are equivalent, but in Go, wget and older requests a leading dot excludes the apex domain, and in Java a dotted entry matches nothing whatsoever. The bare form matches the domain and its subdomains in every implementation except Java, which needs both example.com and *.example.com separated by a pipe.

Does NO_PROXY or no_proxy take precedence?#

It depends on the client, and on the client's version, which is why both should be set to the same value. Go's httpproxy package has read uppercase NO_PROXY first in every released Go toolchain to date, although golang.org/x/net v0.58.0 changed it to prefer lowercase. curl, requests and CPython's urllib prefer the lowercase form, and wget documents only the lowercase name.

Does no_proxy support CIDR ranges?#

curl 7.86.0 and later, Go, and Python requests support CIDR, with requests limited to IPv4. CPython's urllib, wget and Java do not. In all of them, a CIDR entry is only consulted when the URL contains a literal IP address, so it never helps for requests made by hostname.

Why does no_proxy not work for my Kubernetes services?#

Because matching runs against the literal host in the URL, before DNS. Code that calls http://payments/v1/charge presents the host payments, which does not match a .svc.cluster.local entry even though the name resolves to that FQDN. List the short forms your code uses, add the service and pod CIDRs for IP-addressed traffic such as the API server, and include 169.254.169.254 so cloud metadata is not proxied.

Is no_proxy case sensitive?#

The hostname comparison is case insensitive in curl, Go, CPython's urllib, wget and Java. Python requests is the exception in practice: it lowercases the URL host but not the entries in the list, so an entry written in uppercase will never match anything.

Primary sources#

Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.

  1. curl manual page, --noproxy
  2. curl noproxy matching implementation (lib/noproxy.c)
  3. golang.org/x/net/http/httpproxy package documentation
  4. golang.org/x/net/http/httpproxy source
  5. Python requests, should_bypass_proxies (src/requests/utils.py)
  6. CPython urllib.request proxy_bypass_environment
  7. GNU Wget manual, proxy environment variables
  8. Java networking properties, http.nonProxyHosts
  9. httpoxy advisory (CVE-2016-5385, CVE-2016-1000110)

Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.

More in troubleshooting proxies#