From b95807ec90509e1fa9044073bf625ef23c75ca21 Mon Sep 17 00:00:00 2001 From: Molty Date: Tue, 8 Sep 2026 10:27:55 -0700 Subject: [PATCH] fix(embed): honor proxy env for non-loopback endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedding client set Proxy: nil on its http.Transport — intended to keep local Ollama instances off corporate proxies, but it also disabled proxy env for every remote endpoint. Behind credential gateways that inject auth at the proxy boundary (a transparent HTTPS_PROXY), remote OpenAI-compatible providers became unreachable: the client bypassed the gateway, then failed with 401 because the key only exists gateway-side. Route by endpoint instead: loopback (localhost / 127.0.0.0/8 / ::1) never uses a proxy; everything else resolves through the standard HTTPS_PROXY/HTTP_PROXY/NO_PROXY environment. The loopback branch returns an explicit no-proxy resolver rather than nil, which panics when the Transport invokes it. --- internal/memory/embed/ollama.go | 21 +++++++++++++-- internal/memory/embed/openai_test.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/internal/memory/embed/ollama.go b/internal/memory/embed/ollama.go index f1aef461..fe437806 100644 --- a/internal/memory/embed/ollama.go +++ b/internal/memory/embed/ollama.go @@ -105,8 +105,11 @@ func NewClientWithModel(model string) *Client { http: &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ - // Bypass system proxy for localhost connections. - Proxy: nil, + // Honor proxy env for remote endpoints (credential + // gateways inject auth at the proxy boundary); bypass + // it for localhost/loopback servers, where a stray + // HTTPS_PROXY would only get in the way. + Proxy: proxyFunc(endpoint), DialContext: (&net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, @@ -116,6 +119,20 @@ func NewClientWithModel(model string) *Client { } } +// proxyFunc returns an HTTP proxy resolver for the embedding endpoint. +// Loopback endpoints never use a proxy; everything else follows the +// standard HTTPS_PROXY/HTTP_PROXY/NO_PROXY environment resolution. +func proxyFunc(endpoint string) func(*http.Request) (*url.URL, error) { + if u, err := url.Parse(endpoint); err == nil { + if host := u.Hostname(); host == "localhost" || net.ParseIP(host).IsLoopback() { + // Never proxy the loopback: return an explicit no-proxy + // resolver rather than nil (a nil Proxy panics when invoked). + return func(*http.Request) (*url.URL, error) { return nil, nil } + } + } + return http.ProxyFromEnvironment +} + // Protocol returns the active wire protocol. func (c *Client) Protocol() Protocol { return c.protocol diff --git a/internal/memory/embed/openai_test.go b/internal/memory/embed/openai_test.go index 71d18c5b..bb9f1186 100644 --- a/internal/memory/embed/openai_test.go +++ b/internal/memory/embed/openai_test.go @@ -256,3 +256,42 @@ func TestOpenAIAvailableNoFallbackOnServerError(t *testing.T) { t.Fatalf("expected no embedding probe after 500 models route, got %d", embedRequests) } } + +func TestOpenAIProxyEnvHonoredForRemoteEndpoints(t *testing.T) { + // Remote endpoints must resolve their proxy from the environment: + // credential gateways inject auth at the proxy boundary. + t.Setenv("MNEMON_EMBED_ENDPOINT", "http://remote.example.test:18000/v1") + t.Setenv("HTTPS_PROXY", "http://proxy.example.test:3128") + c := NewClient() + req, err := http.NewRequest(http.MethodGet, "https://api.example.test/v1/models", nil) + if err != nil { + t.Fatal(err) + } + proxyURL, err := c.http.Transport.(*http.Transport).Proxy(req) + if err != nil { + t.Fatalf("resolve proxy: %v", err) + } + if proxyURL == nil || proxyURL.Host != "proxy.example.test:3128" { + t.Fatalf("expected env proxy for remote endpoint, got %v", proxyURL) + } +} + +func TestOllamaLoopbackBypassesProxyEnv(t *testing.T) { + // A loopback endpoint must not be routed through an environment + // proxy, even when HTTPS_PROXY is set (local Ollama behind a stray + // corporate proxy would otherwise break). + t.Setenv("MNEMON_EMBED_ENDPOINT", "http://127.0.0.1:11434") + t.Setenv("HTTPS_PROXY", "http://proxy.example.test:3128") + c := NewClient() + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:11434/api/tags", nil) + if err != nil { + t.Fatal(err) + } + proxyURL, err := c.http.Transport.(*http.Transport).Proxy(req) + if err != nil { + t.Fatalf("resolve proxy: %v", err) + } + if proxyURL != nil { + t.Fatalf("expected no proxy for loopback endpoint, got %v", proxyURL) + } +}