Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions internal/runtime/executor/antigravity_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,11 @@ func NewAntigravityExecutor(cfg *config.Config) *AntigravityExecutor {
// It is initialized once via antigravityTransportOnce to avoid leaking a new connection pool
// (and the goroutines managing it) on every request.
var (
antigravityTransport *http.Transport
antigravityTransportOnce sync.Once
antigravityTransport *http.Transport
antigravityTransportOnce sync.Once
antigravityEnvironmentProxyTransport = sync.OnceValue(func() *http.Transport {
return cloneTransportWithHTTP11(helps.EnvironmentProxyTransport())
})
)

func cloneTransportWithHTTP11(base *http.Transport) *http.Transport {
Expand Down Expand Up @@ -153,6 +156,10 @@ func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cli

// Preserve proxy settings from proxy-aware transports while forcing HTTP/1.1.
if transport, ok := client.Transport.(*http.Transport); ok {
if transport == helps.EnvironmentProxyTransport() {
client.Transport = antigravityEnvironmentProxyTransport()
return client
}
client.Transport = cloneTransportWithHTTP11(transport)
}
return client
Expand Down
59 changes: 59 additions & 0 deletions internal/runtime/executor/antigravity_executor_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package executor

import (
"context"
"net/http"
"os"
"testing"

"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
)

func setAntigravityEnvironmentProxy(t *testing.T, proxyURL string) {
t.Helper()

for _, key := range []string{"HTTP_PROXY", "HTTPS_PROXY"} {
oldValue, hadValue := os.LookupEnv(key)
if err := os.Setenv(key, proxyURL); err != nil {
t.Fatalf("Setenv(%s): %v", key, err)
}
cleanupKey := key
cleanupOldValue := oldValue
cleanupHadValue := hadValue
t.Cleanup(func() {
if cleanupHadValue {
_ = os.Setenv(cleanupKey, cleanupOldValue)
return
}
_ = os.Unsetenv(cleanupKey)
})
}
}

func TestNewAntigravityHTTPClientReusesSharedEnvironmentProxyTransport(t *testing.T) {
setAntigravityEnvironmentProxy(t, "http://env-proxy.example.com:8080")

clientA := newAntigravityHTTPClient(context.Background(), &config.Config{}, &cliproxyauth.Auth{}, 0)
clientB := newAntigravityHTTPClient(context.Background(), &config.Config{}, &cliproxyauth.Auth{}, 0)

transportA, okA := clientA.Transport.(*http.Transport)
if !okA {
t.Fatalf("clientA transport type = %T, want *http.Transport", clientA.Transport)
}
transportB, okB := clientB.Transport.(*http.Transport)
if !okB {
t.Fatalf("clientB transport type = %T, want *http.Transport", clientB.Transport)
}

if transportA != transportB {
t.Fatal("expected Antigravity environment proxy transport to be shared across clients")
}
if transportA == helps.EnvironmentProxyTransport() {
t.Fatal("expected Antigravity transport to use its HTTP/1.1 clone, not the generic environment proxy transport")
}
if transportA.ForceAttemptHTTP2 {
t.Fatal("expected Antigravity transport to keep HTTP/2 disabled")
}
}
43 changes: 41 additions & 2 deletions internal/runtime/executor/helps/proxy_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package helps
import (
"context"
"net/http"
"os"
"strings"
"sync"
"time"

"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
Expand All @@ -12,10 +14,22 @@ import (
log "github.com/sirupsen/logrus"
)

var environmentProxyKeys = []string{"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"}

var sharedEnvironmentProxyTransport = sync.OnceValue(func() *http.Transport {
if transport, ok := http.DefaultTransport.(*http.Transport); ok && transport != nil {
clone := transport.Clone()
clone.Proxy = http.ProxyFromEnvironment
return clone
}
return &http.Transport{Proxy: http.ProxyFromEnvironment}
})

// NewProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority:
// 1. Use auth.ProxyURL if configured (highest priority)
// 2. Use cfg.ProxyURL if auth proxy is not configured
// 3. Use RoundTripper from context if neither are configured
// 3. Use environment proxy settings if neither are configured
// 4. Use RoundTripper from context if no explicit or environment proxy is configured
//
// Parameters:
// - ctx: The context containing optional RoundTripper
Expand Down Expand Up @@ -53,7 +67,14 @@ func NewProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip
log.Debugf("failed to setup proxy from URL: %s, falling back to context transport", proxyURL)
}

// Priority 3: Use RoundTripper from context (typically from RoundTripperFor)
// Priority 3: Use environment proxy settings explicitly so all callers share
// the same fallback semantics instead of relying on http.DefaultTransport.
if environmentProxyConfigured() {
httpClient.Transport = newEnvironmentProxyTransport()
return httpClient
}

// Priority 4: Use RoundTripper from context (typically from RoundTripperFor)
if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil {
httpClient.Transport = rt
}
Expand All @@ -77,3 +98,21 @@ func buildProxyTransport(proxyURL string) *http.Transport {
}
return transport
}

func environmentProxyConfigured() bool {
for _, key := range environmentProxyKeys {
if strings.TrimSpace(os.Getenv(key)) != "" {
return true
}
}
return false
}

func newEnvironmentProxyTransport() *http.Transport {
return sharedEnvironmentProxyTransport()
}

// EnvironmentProxyTransport returns the shared transport used for environment proxy fallback.
func EnvironmentProxyTransport() *http.Transport {
return newEnvironmentProxyTransport()
}
93 changes: 93 additions & 0 deletions internal/runtime/executor/helps/proxy_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,35 @@ package helps
import (
"context"
"net/http"
"os"
"testing"

"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
)

func setEnvironmentProxy(t *testing.T, proxyURL string) {
t.Helper()

for _, key := range []string{"HTTP_PROXY", "HTTPS_PROXY"} {
oldValue, hadValue := os.LookupEnv(key)
if err := os.Setenv(key, proxyURL); err != nil {
t.Fatalf("Setenv(%s): %v", key, err)
}
cleanupKey := key
cleanupOldValue := oldValue
cleanupHadValue := hadValue
t.Cleanup(func() {
if cleanupHadValue {
_ = os.Setenv(cleanupKey, cleanupOldValue)
return
}
_ = os.Unsetenv(cleanupKey)
})
}
}

func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) {
t.Parallel()

Expand All @@ -28,3 +50,74 @@ func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) {
t.Fatal("expected direct transport to disable proxy function")
}
}

func TestNewProxyAwareHTTPClientFallsBackToEnvironmentProxy(t *testing.T) {
setEnvironmentProxy(t, "http://env-proxy.example.com:8080")

client := NewProxyAwareHTTPClient(context.Background(), &config.Config{}, &cliproxyauth.Auth{}, 0)

transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("transport type = %T, want *http.Transport", client.Transport)
}
if transport.Proxy == nil {
t.Fatal("expected environment proxy transport to configure Proxy function")
}
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
if errReq != nil {
t.Fatalf("NewRequest() error = %v", errReq)
}
proxyURL, errProxy := transport.Proxy(req)
if errProxy != nil {
t.Fatalf("transport.Proxy() error = %v", errProxy)
}
if proxyURL == nil || proxyURL.String() != "http://env-proxy.example.com:8080" {
t.Fatalf("proxy URL = %v, want http://env-proxy.example.com:8080", proxyURL)
}
}

func TestNewProxyAwareHTTPClientExplicitProxyWinsOverEnvironmentProxy(t *testing.T) {
setEnvironmentProxy(t, "http://env-proxy.example.com:8080")

client := NewProxyAwareHTTPClient(
context.Background(),
&config.Config{SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://config-proxy.example.com:8080"}},
nil,
0,
)

transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("transport type = %T, want *http.Transport", client.Transport)
}
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
if errReq != nil {
t.Fatalf("NewRequest() error = %v", errReq)
}
proxyURL, errProxy := transport.Proxy(req)
if errProxy != nil {
t.Fatalf("transport.Proxy() error = %v", errProxy)
}
if proxyURL == nil || proxyURL.String() != "http://config-proxy.example.com:8080" {
t.Fatalf("proxy URL = %v, want http://config-proxy.example.com:8080", proxyURL)
}
}

func TestNewProxyAwareHTTPClientReusesEnvironmentProxyTransport(t *testing.T) {
setEnvironmentProxy(t, "http://env-proxy.example.com:8080")

clientA := NewProxyAwareHTTPClient(context.Background(), &config.Config{}, &cliproxyauth.Auth{}, 0)
clientB := NewProxyAwareHTTPClient(context.Background(), &config.Config{}, &cliproxyauth.Auth{}, 0)

transportA, okA := clientA.Transport.(*http.Transport)
if !okA {
t.Fatalf("clientA transport type = %T, want *http.Transport", clientA.Transport)
}
transportB, okB := clientB.Transport.(*http.Transport)
if !okB {
t.Fatalf("clientB transport type = %T, want *http.Transport", clientB.Transport)
}
if transportA != transportB {
t.Fatal("expected environment proxy transport to be shared across clients")
}
}
Loading