fix: Bypass proxy for loopback addresses to prevent localhost connection failures (#194)

When users have HTTP_PROXY/HTTPS_PROXY set in their shell (e.g. corporate
proxy), PMG's upstream transport routes all traffic through that external
proxy — including requests to localhost/127.0.0.1. The external proxy
cannot reach the user's loopback, causing EFAULT/ConnectionRefused errors.

This adds loopback bypass in two places:
- Proxy upstream transport skips external proxy for localhost/127.0.0.1/::1
- Child process env gets NO_PROXY so proxy-aware libs (axios) skip PMG's
  proxy for loopback addresses

Fixes #193

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sahil Bansal
2026-04-01 19:28:25 +05:30
committed by GitHub
co-authored by Claude Opus 4.6
parent 5fdc3a03ae
commit 635e40cc1f
3 changed files with 46 additions and 2 deletions
+13 -2
View File
@@ -134,6 +134,15 @@ func NewProxyServer(config *ProxyConfig) (ProxyServer, error) {
return ps, nil
}
func proxyWithLoopbackBypass(req *http.Request) (*url.URL, error) {
host := req.URL.Hostname()
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return nil, nil
}
return http.ProxyFromEnvironment(req)
}
func newUpstreamTransport(config *ProxyConfig) *http.Transport {
dialer := &net.Dialer{
Timeout: config.ConnectTimeout,
@@ -141,7 +150,9 @@ func newUpstreamTransport(config *ProxyConfig) *http.Transport {
// Proxy honours the environment (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) so
// that PMG works in enterprise environments that require a corporate
// upstream proxy to reach the internet.
// upstream proxy to reach the internet. Loopback addresses are always
// bypassed to avoid routing localhost traffic through an external proxy,
// which would fail because the proxy can't reach the user's localhost.
//
// ForceAttemptHTTP2 is required because Go's http.Transport silently
// disables HTTP/2 when a custom TLSClientConfig or DialContext is set.
@@ -157,7 +168,7 @@ func newUpstreamTransport(config *ProxyConfig) *http.Transport {
// MaxIdleConnsPerHost is raised from the default of 2 to improve
// connection reuse.
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Proxy: proxyWithLoopbackBypass,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxConnsPerHost: 100,