Net::HTTP automatically bypasses environment-configured proxies when the destination resolves to a loopback address.
Could an explicit opt-in be added that allows Net::HTTP to use an environment proxy for localhost and other loopback destinations while preserving the current default behavior?
Current behavior
Given:
ENV["http_proxy"] = "http://127.0.0.1:8888"
require "net/http"
http = Net::HTTP.new("localhost", 7700)
p http.proxy_from_env? # true
p http.proxy? # false
Net::HTTP discovers proxies through URI::Generic#find_proxy. That method resolves the destination hostname and unconditionally returns nil for addresses matching 127.* or ::1:
https://github.com/ruby/uri/blob/master/lib/uri/generic.rb
This happens independently of NO_PROXY. An empty NO_PROXY therefore cannot request that loopback traffic use the proxy.
Supplying the proxy explicitly to Net::HTTP.new works, but that requires applications and libraries to replace normal environment-based proxy discovery.
Use case
I encountered this in an OpenAI Codex sandbox on macOS.
The sandbox injects an HTTP proxy into the process environment and permits network access through that proxy. Direct socket connections are restricted by the sandbox policy, including direct connections to a local service in this configuration.
The result was:
curl http://localhost:7700/health
# succeeds through the injected proxy
Net::HTTP.get(URI("http://localhost:7700/health"))
# Errno::EPERM: Operation not permitted - connect(2) for 127.0.0.1:7700
The local destination was allowed by the sandbox configuration and was reachable through its proxy broker. However, Ruby detected that localhost resolved to loopback, bypassed the environment proxy, and attempted the prohibited direct connection.
This also affects libraries that use Net::HTTP internally. They may not expose a way to pass an explicit proxy, while environment variables are the standard integration point provided by sandboxed and containerized environments.
curl/libcurl behaves differently: it uses the configured proxy for localhost unless NO_PROXY or CURLOPT_NOPROXY excludes it.
Behavior in other HTTP libraries
There is no universal standard for proxying loopback destinations, but several implementations either make the behavior configurable or rely entirely on the normal proxy exclusion configuration.
- Go
net/http: ProxyFromEnvironment also bypasses localhost and loopback addresses unconditionally. There is no environment variable that reverses this decision. Applications can override it by configuring Transport.Proxy, for example with http.ProxyURL.
- Java: the default
http.nonProxyHosts value excludes localhost, 127.*, and [::1]. Unlike Ruby’s hardcoded check, this property can be replaced or cleared when loopback traffic needs to use the proxy.
- .NET: local bypass behavior is controlled by
WebProxy.BypassProxyOnLocal. Applications can set it to false to send local destinations through the proxy.
- curl/libcurl: loopback destinations are not implicitly excluded. They use the configured proxy unless
NO_PROXY or CURLOPT_NOPROXY explicitly matches the destination.
- Python
urllib.request: when using environment proxy settings, bypasses are generally controlled through NO_PROXY. Without a matching exclusion, localhost may use the proxy, although system proxy settings can affect behavior on macOS and Windows.
Ruby is therefore not alone in bypassing loopback by default. However, unlike Java and .NET, its environment-proxy behavior does not expose a switch to disable that bypass. Go has a similar unconditional rule in ProxyFromEnvironment, but provides a replaceable proxy-selection callback at the HTTP transport level. curl/libcurl takes the opposite approach and leaves loopback behavior entirely to the proxy exclusion configuration.
Requested behavior
It would be useful to have an explicit, backwards-compatible way to disable the automatic loopback bypass when proxy configuration comes from the environment.
For example, this could be:
- a
Net::HTTP option;
- an option passed to
URI::Generic#find_proxy; or
- a dedicated environment setting such as
NET_HTTP_PROXY_LOOPBACK=1.
The exact API is less important than retaining the current default behavior while allowing controlled environments to opt into proxying loopback destinations.
NO_PROXY cannot express this because it only adds destinations that bypass the proxy; it cannot remove Ruby’s built-in loopback exclusion.
Workaround
Applications can parse the proxy environment variables themselves and pass the proxy explicitly:
proxy = URI(ENV.fetch("http_proxy"))
http = Net::HTTP.new(
"localhost",
7700,
proxy.host,
proxy.port,
proxy.user,
proxy.password
)
This works, but duplicates proxy parsing and bypass behavior, does not help libraries that instantiate Net::HTTP internally, and couples application code to sandbox infrastructure.
Versions
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25]
net-http 0.9.1
uri 1.1.1
I realize that the actual loopback check currently belongs to URI::Generic#find_proxy. I am filing this here because it surfaces through Net::HTTP’s environment-proxy behavior, but I am happy to move the request to ruby/uri if that is the preferred location.
Net::HTTPautomatically bypasses environment-configured proxies when the destination resolves to a loopback address.Could an explicit opt-in be added that allows
Net::HTTPto use an environment proxy for localhost and other loopback destinations while preserving the current default behavior?Current behavior
Given:
Net::HTTPdiscovers proxies throughURI::Generic#find_proxy. That method resolves the destination hostname and unconditionally returnsnilfor addresses matching127.*or::1:https://github.com/ruby/uri/blob/master/lib/uri/generic.rb
This happens independently of
NO_PROXY. An emptyNO_PROXYtherefore cannot request that loopback traffic use the proxy.Supplying the proxy explicitly to
Net::HTTP.newworks, but that requires applications and libraries to replace normal environment-based proxy discovery.Use case
I encountered this in an OpenAI Codex sandbox on macOS.
The sandbox injects an HTTP proxy into the process environment and permits network access through that proxy. Direct socket connections are restricted by the sandbox policy, including direct connections to a local service in this configuration.
The result was:
The local destination was allowed by the sandbox configuration and was reachable through its proxy broker. However, Ruby detected that
localhostresolved to loopback, bypassed the environment proxy, and attempted the prohibited direct connection.This also affects libraries that use
Net::HTTPinternally. They may not expose a way to pass an explicit proxy, while environment variables are the standard integration point provided by sandboxed and containerized environments.curl/libcurl behaves differently: it uses the configured proxy for localhost unless
NO_PROXYorCURLOPT_NOPROXYexcludes it.Behavior in other HTTP libraries
There is no universal standard for proxying loopback destinations, but several implementations either make the behavior configurable or rely entirely on the normal proxy exclusion configuration.
net/http:ProxyFromEnvironmentalso bypasseslocalhostand loopback addresses unconditionally. There is no environment variable that reverses this decision. Applications can override it by configuringTransport.Proxy, for example withhttp.ProxyURL.http.nonProxyHostsvalue excludeslocalhost,127.*, and[::1]. Unlike Ruby’s hardcoded check, this property can be replaced or cleared when loopback traffic needs to use the proxy.WebProxy.BypassProxyOnLocal. Applications can set it tofalseto send local destinations through the proxy.NO_PROXYorCURLOPT_NOPROXYexplicitly matches the destination.urllib.request: when using environment proxy settings, bypasses are generally controlled throughNO_PROXY. Without a matching exclusion, localhost may use the proxy, although system proxy settings can affect behavior on macOS and Windows.Ruby is therefore not alone in bypassing loopback by default. However, unlike Java and .NET, its environment-proxy behavior does not expose a switch to disable that bypass. Go has a similar unconditional rule in
ProxyFromEnvironment, but provides a replaceable proxy-selection callback at the HTTP transport level. curl/libcurl takes the opposite approach and leaves loopback behavior entirely to the proxy exclusion configuration.Requested behavior
It would be useful to have an explicit, backwards-compatible way to disable the automatic loopback bypass when proxy configuration comes from the environment.
For example, this could be:
Net::HTTPoption;URI::Generic#find_proxy; orNET_HTTP_PROXY_LOOPBACK=1.The exact API is less important than retaining the current default behavior while allowing controlled environments to opt into proxying loopback destinations.
NO_PROXYcannot express this because it only adds destinations that bypass the proxy; it cannot remove Ruby’s built-in loopback exclusion.Workaround
Applications can parse the proxy environment variables themselves and pass the proxy explicitly:
This works, but duplicates proxy parsing and bypass behavior, does not help libraries that instantiate
Net::HTTPinternally, and couples application code to sandbox infrastructure.Versions
I realize that the actual loopback check currently belongs to
URI::Generic#find_proxy. I am filing this here because it surfaces throughNet::HTTP’s environment-proxy behavior, but I am happy to move the request toruby/uriif that is the preferred location.