Updated on: June 9, 2025
12 min read
Subtle differences in proxy setting implementations led to surprising problems for a GitLab customer. Here's how we got to the root of it.

If you've used a Web proxy server before, you're probably familiar with
the environment variables http_proxy or HTTP_PROXY. You may be less
familiar with no_proxy, which provides a way to exclude traffic
destined to certain hosts from using the proxy. While HTTP is a
well-defined standard, no standard exists for how clients should handle
these variables. As a result, Web clients support these variables in
subtly different ways. For one GitLab customer, these differences led
to a weekend of troubleshooting that uncovered why certain services
stopped communicating.
A proxy server acts as an intermediary between your computer or network and the internet. When you send a request to access a website or other online resource, that request first goes to the proxy server. The proxy server then forwards the request to the actual destination and delivers the response back to you. Proxies can serve various purposes, including improving security, enhancing privacy, and controlling internet usage.
Let's now look at what proxy server environment variables are, and how to define exemptions and handle exclusions with no_proxy.
Today, most Web clients support connection to proxy servers via environment variables:
http_proxy / HTTP_PROXYhttps_proxy / HTTPS_PROXYno_proxy / NO_PROXYThese variables tell the client what URL should be used to access the
proxy servers and which exceptions should be made. For example, if you
had a proxy server listening on http://alice.example.com:8080, you
might use it via:
export http_proxy=http://alice.example.com:8080
Which proxy server gets used if troublesome Bob also defines the
all-caps version, HTTP_PROXY?
export HTTP_PROXY=http://bob.example.com:8080
The answer surprised us: it depends. In some cases, the Alice proxy wins, and in other cases Bob wins. We'll discuss the details later.
no_proxyWhat happens if you want to make exceptions? For example, suppose you
want to use a proxy server for everything but internal.example.com and
internal2.example.com. That's where the no_proxy variable comes into
play. Then you would define no_proxy as follows:
export no_proxy=internal.example.com,internal2.example.com
no_proxyWhat if you want to exclude IP addresses? Can you use asterisks or
wildcards? Can you use CIDR blocks (e.g. 192.168.1.1/32)? The answer
again: it depends.
Let's dig into the evolution of proxy variables, and how they are used today.
In 1994, most Web clients used CERN's libwww, which supported http_proxy and the no_proxy environment variables.
libwww only used the lowercase form of http_proxy, and the no_proxy syntax was
simple:
no_proxy is a comma- or space-separated list of machine
or domain names, with optional :port part. If no :port
part is present, it applies to all ports on that domain.
Example:
no_proxy="cern.ch,some.domain:8001"
New clients emerged that added their own HTTP implementations without
linking libwww. In January 1996, Hrvoje Niksic released
geturl, the predecessor of what is now wget. A month later,
geturl, added support for http_proxy in v1.1.
In May 1996, geturl v1.3 added support for no_proxy. Just as with
libwww, geturl only supported the lowercase form.
In January 1998, Daniel Stenberg released curl v5.1, which supported the http_proxy and no_proxy variables.
In addition, curl allowed the uppercase forms, HTTP_PROXY and NO_PROXY.
Plot twist: In March 2009, curl v7.19.4 dropped support for the
uppercase form of HTTP_PROXY due to security concerns. However, while curl ignores HTTP_PROXY, HTTPS_PROXY still works today.
Fast-forward to today. As my colleague Nourdin el Bacha researched, we can see that how these proxy server variables are handled varies, depending on what language or tool you are using.
Knowing how proxy variables are handled across languages allows you to set them so that they work properly. Here’s a quick rundown.
http_proxy and https_proxyIn the following table, each row represents a supported behavior, while
each column holds the tool (e.g. curl) or language (e.g. Ruby) to
which it applies:
| curl | wget | Ruby | Python | Go | |
|---|---|---|---|---|---|
http_proxy |
Yes | Yes | Yes | Yes | Yes |
HTTP_PROXY |
No | No | Yes (warning) | Yes (if REQUEST_METHOD not in env) |
Yes |
https_proxy |
Yes | Yes | Yes | Yes | Yes |
HTTPS_PROXY |
Yes | No | Yes | Yes | Yes |
| Case precedence | lowercase | lowercase only | lowercase | lowercase | Uppercase |
| Reference | source | source | source | source | source |
Note that http_proxy and https_proxy are always supported across the
board, while HTTP_PROXY is not always supported. Python (via urllib) complicates
the picture even more: HTTP_PROXY can be used as long as
REQUEST_METHOD is not defined in the environment.
While you might expect environment variables to be all-caps,
http_proxy came first, so that's the de facto standard. When in doubt,
go with the lowercase form because that's universally supported.
Instead of environment variables, Java uses system properties. This avoids case issues entirely.
Unlike most implementations, Go tries the uppercase version before falling back to the lowercase version. We will see later why that caused issues for one GitLab customer.
no_proxy formatSome users have discussed the lack of the no_proxy specification in this issue. As
no_proxy specifies an exclusion list, many questions arise about
how it behaves. For example, suppose your no_proxy configuration is defined:
export no_proxy=example.com
Does this mean that the domain must be an exact match, or will
subdomain.example.com also match against this configuration? The
following table shows the state of various implementations. It turns out
all implementations will match suffixes properly, as shown in the
Matches suffixes? row:
| curl | wget | Ruby | Python | Go | Java | |
|---|---|---|---|---|---|---|
no_proxy |
Yes | Yes | Yes | Yes | Yes | No* |
NO_PROXY |
Yes | No | Yes | Yes | Yes | No* |
| Case precedence | lowercase | lowercase only | lowercase | lowercase | Uppercase | N/A |
| Matches suffixes? | Yes | Yes | Yes | Yes | Yes | No |
Strips leading .? |
Yes | No | Yes | Yes | No | No |
* matches all hosts? |
Yes | No | No | Yes | Yes | Yes |
| Supports regexes? | No | No | No | No | No | No |
| Supports CIDR blocks? | No | No | Yes | No | Yes | No |
| Detects loopback IPs? | No | No | No | No | Yes | No |
| Resolves IP addresses? | No | No | Yes | No | Yes | No |
| Reference | source | source | source | source | source | documentation |
http.nonProxyHosts system property.However, if there is a leading . in the no_proxy setting, the
behavior varies. For example, curl and wget behave
differently. curl will always strip the leading . and match against
a domain suffix. This call bypasses the proxy:
$ env https_proxy=http://non.existent/ no_proxy=.gitlab.com curl https://gitlab.com
<html><body>You are being <a href="https://about.gitlab.com/">redirected</a>.</body></html>
However, wget does not strip the leading . and performs an exact
string match against a hostname. As a result, wget attempts to use a
proxy if a top-level domain is used:
$ env https_proxy=http://non.existent/ no_proxy=.gitlab.com wget https://gitlab.com
Resolving non.existent (non.existent)... failed: Name or service not known.
wget: unable to resolve host address 'non.existent'
In all implementations, regular expressions are never supported. I suspect using regexes complicates matters further, because regexes have their own flavors (e.g. PCRE, POSIX, etc.). Using regexes also introduces potential performance and security issues.
In some cases, setting no_proxy to * effectively disables proxies
altogether, but this is not a universal rule.
Only Ruby performs a DNS lookup to resolve a hostname to an IP address when deciding if a proxy should be used. Be careful if you use IP addresses with Ruby because it’s possible a hostname may resolve to an excluded IP address. In general, do not specify IP addresses in no_proxy variable unless you expect that the IPs are explicitly used by the client.
The same holds true for CIDR blocks, such as 18.240.0.1/24. CIDR
blocks only work when the request is directly made to an IP
address. Only Go and Ruby allow CIDR blocks. Unlike other
implementations, Go even automatically disables the use of a proxy if it
detects a loopback IP addresses.
Discrepancies in proxy environment variable handling, particularly between Ruby and Go, can lead to a real-world issues where Git pushes worked via the command line but failed in the web UI for a GitLab customer. Understanding these inconsistencies is crucial for troubleshooting and configuring applications that operate across multiple languages within corporate networks utilizing proxy servers.
If you have an application written in multiple languages that needs to work behind a corporate firewall with a proxy server, you may need to pay attention to these differences. For example, GitLab is composed of a few services written in Ruby and Go. One customer set its proxy configuration to something like the following:
HTTP_PROXY: http://proxy.company.com
HTTPS_PROXY: http://proxy.company.com
NO_PROXY: .correct-company.com
The customer reported the following issue with GitLab:
git push from the command line workedOur support engineers discovered that due to a Kubernetes configuration issue, a few stale values lingered. The pod actually had an environment that looked something like:
HTTP_PROXY: http://proxy.company.com
HTTPS_PROXY: http://proxy.company.com
NO_PROXY: .correct-company.com
no_proxy: .wrong-company.com
The inconsistent definitions in no_proxy and NO_PROXY set off red
flags, and we could have resolved the issue by making them consistent or
removing the incorrect entry. But let's drill into what happened.
Remember from above that:
As a result, services written in Go, such as GitLab Workhorse, had the
correct proxy configuration. A git push from the command line worked
fine because the Go services primarily handled this activity:
The gRPC call in step 2 never attempted to use the proxy because
no_proxy was configured properly to connect directly to Gitaly.
However, when a user makes a change in the UI, Gitaly forwards the
request to a gitaly-ruby service, which is written in
Ruby. gitaly-ruby makes changes to the repository and reports back
via a gRPC call back to its parent process. However,
as seen in step 4 below, the reporting step didn't happen:
Because gRPC uses HTTP/2 as the underlying transport, gitaly-ruby
attempted a CONNECT to the proxy since it was configured with the wrong
no_proxy setting. The proxy immediately rejected this HTTP request,
causing the failure in the Web UI push case.
Once we eliminated the lowercase no_proxy from the environment, pushes
from the UI worked as expected, and gitaly-ruby connected directly to
the parent Gitaly process. Step 4 worked properly in the diagram below:
We also discovered that gRPC does not support HTTPS proxies. This again subtly affects the behavior of the system depending on how HTTPS_PROXY is set.
HTTPS_proxyNote that the customer set HTTPS_PROXY to an unencrypted HTTP proxy;
notice that http:// is used instead of https://. While this isn't
ideal from a security standpoint, some people do this to avoid the
hassle of clients failing due to TLS certificate verification issues.
Ironically, if an HTTPS proxy were specified, we would not have seen this problem. If an HTTPS proxy is used, gRPC will ignore this setting since HTTPS proxies are not supported.
I think we can all agree that one should never define inconsistent values with lowercase and uppercase proxy settings. However, if you ever have to manage a stack written in multiple languages, you might need to consider setting HTTP proxy configurations to the lowest common denominator.
http_proxy and https_proxyHTTP_PROXY is not always supported or recommended.
no_proxy
Use lowercase form.
Use comma-separated hostname:port values.
IP addresses are okay, but hostnames are never resolved.
Suffixes are always matched (e.g. example.com will match test.example.com).
If top-level domains need to be matched, avoid using a leading dot (.).
Avoid using CIDR matching since only Go and Ruby support that.
no_proxyKnowing the least common denominator can help avoid issues if these
definitions are copied for different Web clients. But should no_proxy
and the other proxy settings have a documented standard rather than an
ad hoc convention? The list below may serve as a starting point for a
proposal:
http_proxy should be searched before HTTP_PROXY).hostname:port values.
* to match all hosts..) and match against domain suffixes.no_proxy).It's been over 25 years since the first Web proxy was released. While
the basic mechanics of configuring a Web client via environment
variables have not changed much, a number of subtleties have emerged
across different implementations. We saw for one customer, erroneously
defining conflicting no_proxy and NO_PROXY variables led to hours of
troubleshooting due to the differences with which Ruby and Go parse
these settings. We hope highlighting these differences will avoid future
issues in your production stack, and we hope that Web client maintainers
will standardize the behavior to avoid such issues in the first place.