Skip to content

Add support for HTTP_PROXY, HTTPS_PROXY, and NO_PROXY#1260

Open
peterguy wants to merge 22 commits intomainfrom
peterguy/support-http_proxy
Open

Add support for HTTP_PROXY, HTTPS_PROXY, and NO_PROXY#1260
peterguy wants to merge 22 commits intomainfrom
peterguy/support-http_proxy

Conversation

@peterguy
Copy link
Contributor

@peterguy peterguy commented Feb 25, 2026

Adds support for the standard environment variables HTTP_PROXY, HTTPS_PROXY, and NO_PROXY.

SRC_PROXY still takes precedence, including over NO_PROXY.

Also added fixes for connecting to TLS-enabled proxies, where we want to connect to the SG API using HTTP/2, but many proxies support only HTTP/1.1, so we need to connect to the proxy via HTTP/1.1, then connect through the proxy to the SG API using HTTP/2.

Moved endpoint and proxy parsing to the config building so that typos and malformed urls are caught early and errors returned immediately.

Test plan

Added CI tests validating proxy behavior.

Manual testing

Install mitmproxy and squid.

Create an SSL cert for squid:

openssl req -x509 -newkey rsa:2048 \
  -keyout squid-key.pem -out squid-cert.pem \
  -days 365 -nodes -subj "/CN=localhost"
cat squid-key.pem squid-cert.pem > squid-proxy.pem

Create a config for squid:

cat << EOF >squid.conf
https_port 8445 cert=./squid-proxy.pem
sslproxy_session_cache_size 0
acl localnet src 127.0.0.1/32
http_access allow localhost
acl SSL_ports port 443
acl Safe_ports port 80
acl Safe_ports port 443
acl Safe_ports port 1025-65535
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access deny all
http_port 3128
EOF

Run mitmproxy - mitmproxy -v - in one terminal.

Run squid - squid -f ./squid.conf -N -d 1 - in another terminal.

In a third terminal, in the src-cli repo, build the binary: go build -o ./src-cli ./cmd/src

Ensure you have SRC_ENDPOINT and SRC_ACCESS_TOKEN set in your environment, and unset SRC_PROXY.

Try connecting with each proxy (mitmproxy is 8080, squid is 8445):

for x in http://localhost:8080 https://localhost:8445
do
HTTPS_PROXY=${x} ./src-cli login --insecure-skip-verify=true
SRC_PROXY=${x} ./src-cli login --insecure-skip-verify=true
done

You should get "Authenticated as [USER] on [ENDPOINT]" messages in your terminal, and in the mitmproxy and squid terminals, you should see activity.

This tests connecting through a proxy that connects with http (mitmproxy), and one that is TLS-enabled (squid).

Ensure that NO_PROXY is ignored when using SRC_PROXY and honored when using HTTPS_PROXY:

HTTPS_PROXY=http://localhost:8080 NO_PROXY=[HOSTNAME FROM ENDPOINT] ./src-cli login

You should get a successful login, but no entry in the mitmproxy log.

SRC_PROXY=http://localhost:8080 NO_PROXY=[HOSTNAME FROM ENDPOINT] ./src-cli login

You should get a successful login and also see an entry in the mitmproxy log.

Closes https://linear.app/sourcegraph/issue/CPL-236/src-cli-support-standard-http-proxy

@keegancsmith keegancsmith requested a review from a team February 26, 2026 07:12
func envProxyURL(endpoint string) *url.URL {
u, err := url.Parse(endpoint)
if err != nil || u.Scheme == "" || u.Host == "" {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about logging the error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. I re-did the URL parsing for both the endpoint and the proxy - pushing that out to main.go when the program loads, so that users get notified immediately if there are parsing problems for either of them.

}
proxyURL, err := http.ProxyFromEnvironment(&http.Request{URL: u})
if err != nil || proxyURL == nil {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about logging the error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. I re-did the URL parsing for both the endpoint and the proxy - pushing that out to main.go when the program loads, so that users get notified immediately if there are parsing problems for either of them.

Copy link
Member

@keegancsmith keegancsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Inline comments, but otherwise LGTM

if opts.ProxyURL != nil || opts.ProxyPath != "" {
// Explicit SRC_PROXY configuration takes precedence.
transport = withProxyTransport(transport, opts.ProxyURL, opts.ProxyPath)
} else if proxyURL := envProxyURL(opts.Endpoint); proxyURL != nil && proxyURL.Scheme == "https" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does https:// not work without intervention? (as documented below for http and socks5)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're connecting to the SG API using HTTP/2, the standard transport tries to connect to the TLS-enabled proxy using HTTP/2 also, which fails if the proxy doesn't support HTTP/2. I haven't found a proxy for local testing that does support HTTP/2; I suspect that most/all proxies that are TLS-enabled don't support HTTP/2.

// not already present (443 for https, 80 for http).
func proxyDialAddr(proxyURL *url.URL) string {
addr := proxyURL.Host
if _, _, err := net.SplitHostPort(addr); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you ignoring the host here and instead assuming addr is just the host? I think this could be a bug right? IE if your proxy url has a port specified won't this break?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's confusing. I wonder if I goofed and deleted a line or something. All the tests were passing; guess I'd better revisit those also! 😊

// proxyDialAddr returns proxyURL.Host with a default port appended if one is
// not already present (443 for https, 80 for http).
func proxyDialAddr(proxyURL *url.URL) string {
addr := proxyURL.Host
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should be using the Hostname and Port methods instead of net.SplitHostPort. For example, I think ipv6 addresses would break since the way they are represented in a URL are different to net.SplitHostPort expects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, the docs for net.SplitHostPort mention IPV6; I'll add some test cases to see if it does work.

Comment on lines +101 to +125
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("proxy listen: %v", err)
}

srv := &http.Server{Handler: handler}

if useTLS {
cert := generateTestCert(t, "127.0.0.1")
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
go srv.ServeTLS(ln, "", "")
} else {
go srv.Serve(ln)
}
t.Cleanup(func() { srv.Close() })

// Wait for the server to be ready
waitForServerReady(t, ln.Addr().String(), useTLS, 5*time.Second)

scheme := "http"
if useTLS {
scheme = "https"
}
pURL, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, ln.Addr().String()))
return pURL, ch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can greatly simplify code here (and remove helpers like waitForServerReady and generateTestCert) by using the httptest package. It already provides functions to listen on TLS/etc and does things like wait for the server to be listening.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This advice still stands

{"https with port", "https://proxy.example.com:8443", "proxy.example.com:8443"},
{"https without port", "https://proxy.example.com", "proxy.example.com:443"},
{"http with port", "http://proxy.example.com:8080", "proxy.example.com:8080"},
{"http without port", "http://proxy.example.com", "proxy.example.com:80"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get our favourite AI's to generate some example with ipv4, ipv6 and localhost.

@keegancsmith
Copy link
Member

You might wanna rebase, I think william just did some CI updates which include things like go-lint/etc.

@peterguy
Copy link
Contributor Author

Thanks for all of your commentary; I forgot to open this PR in draft mode! Been trying to address what looks like flakey tests in the Ubuntu CI runner; I'll go through your very helpful comments soon.

@peterguy
Copy link
Contributor Author

peterguy commented Feb 27, 2026

This change is part of the following stack:

Change managed by git-spice.

@peterguy peterguy force-pushed the peterguy/support-http_proxy branch from b1d6176 to 05f0fbb Compare February 27, 2026 00:48
@keegancsmith
Copy link
Member

@peterguy alright just re-request review when ready.

…y edge cases of silently dropped/ignored pieces of the response from the proxy
…e sure it'll try HTTP/2 by setting NextProtos
… env up front so that typos and malformed urls will surface immediately
…e proxy in the config is now the one gathered from either SRC_PROXY or the standard env variables
@peterguy peterguy force-pushed the peterguy/support-http_proxy branch from 622bd60 to 3536377 Compare February 27, 2026 19:40
Copy link
Member

@keegancsmith keegancsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes since I just wanna re-review once you have addressed the comments. FYI I pushed some commits to resolve the CI issues.

}
if !isValidUDS {
return nil, errors.Newf("invalid proxy socket: %s", path)
return nil, errors.Newf("Invalid proxy socket: %s", path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you changed this is some other places as well. go convention is lowercase on error messages since they get combined. eg My outer error: My inner error doesn't read as nicely as my outer error: my inner error

Comment on lines +110 to +111
// For http:// and socks5:// proxies, the cloned
// transport's default Proxy handles them correctly without intervention.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I set SRC_PROXY to a http:// or socks5:// though, won't the default transport ignore it?

Comment on lines +101 to +125
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("proxy listen: %v", err)
}

srv := &http.Server{Handler: handler}

if useTLS {
cert := generateTestCert(t, "127.0.0.1")
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
go srv.ServeTLS(ln, "", "")
} else {
go srv.Serve(ln)
}
t.Cleanup(func() { srv.Close() })

// Wait for the server to be ready
waitForServerReady(t, ln.Addr().String(), useTLS, 5*time.Second)

scheme := "http"
if useTLS {
scheme = "https"
}
pURL, _ := url.Parse(fmt.Sprintf("%s://%s", scheme, ln.Addr().String()))
return pURL, ch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This advice still stands

}
s += fmt.Sprintf(" %s \\\n", shellquote.Join("-d", string(data)))
s += fmt.Sprintf(" %s", shellquote.Join(r.client.opts.Endpoint+"/.api/graphql"))
s += fmt.Sprintf(" %s", shellquote.Join(r.client.opts.EndpointURL.String()+"/.api/graphql"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given you are doing TrimRight before you should probably do it here as well.

Comment on lines +20 to +22
func (c *connWithBufferedReader) Read(p []byte) (int, error) {
return c.r.Read(p)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

net.Conn documents all methods to be concurrency safe. However, bufio.Reader is not. You need to add in a mutex here.

> Multiple goroutines may invoke methods on a Conn simultaneously.


// Read and check the response from the proxy
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
br := bufio.NewReader(conn)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't clear to me why you introduced the buffered reader?

Comment on lines 134 to +137
ProxyURL *url.URL
ProxyPath string
ConfigFilePath string
EndpointURL *url.URL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: but these fields should probably all not be exported. Looks like we don't want to marshal them / they are internally read/set.

Comment on lines +335 to +338
EndpointURL: &url.URL{
Scheme: "https",
Host: "override.com",
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth updating all these test cases for this? I'd consider just not comparing this field in the comparsion in the test.


var cfg *config

// config represents the config format.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Re: line +130]

I think you should marshal into a struct with just the json fields here. Then from your readConfig function return something which just contains the fields you want to read. Then you don't have both Endpoint and EndpointURL in this struct which is a bit weird.

See this comment inline on Graphite.

}
tlsConn := tls.Client(conn, cfg)
if err := tlsConn.HandshakeContext(ctx); err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to call tlsConn.Close here to cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants