feat: add per-channel HTTP transport controls
This commit is contained in:
+176
-40
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
@@ -30,7 +32,7 @@ var (
|
||||
type proxyHTTPClientCache struct {
|
||||
mutex sync.RWMutex
|
||||
clients map[string]*http.Client
|
||||
aliases map[string]string
|
||||
aliases map[string]string // rawProxyURL -> canonicalProxyURL
|
||||
}
|
||||
|
||||
type proxyURLConfig struct {
|
||||
@@ -96,7 +98,7 @@ func newRelayHTTPTransport() *http.Transport {
|
||||
return transport
|
||||
}
|
||||
|
||||
func newRelayHTTPClient(transport *http.Transport) *http.Client {
|
||||
func newRelayHTTPClient(transport http.RoundTripper) *http.Client {
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: checkRedirect,
|
||||
@@ -107,10 +109,14 @@ func newRelayHTTPClient(transport *http.Transport) *http.Client {
|
||||
return client
|
||||
}
|
||||
|
||||
func clientCacheKey(proxyCacheKey string, policy HTTPTransportPolicy) string {
|
||||
return proxyCacheKey + "\x00" + policy.cacheKeyPart()
|
||||
}
|
||||
|
||||
func InitHttpClient() {
|
||||
transport := newRelayHTTPTransport()
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
httpClient = newRelayHTTPClient(transport)
|
||||
policy := defaultHTTPTransportPolicy()
|
||||
httpClient = newDirectHTTPClient(policy, nil)
|
||||
proxyClients.store(clientCacheKey("", policy), httpClient)
|
||||
ssrfProtectedHTTPClient = newProtectedFetchHTTPClient()
|
||||
}
|
||||
|
||||
@@ -177,45 +183,73 @@ func ValidateProxyURL(rawProxyURL string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cache *proxyHTTPClientCache) get(rawCacheKey string) (*http.Client, bool) {
|
||||
func (cache *proxyHTTPClientCache) store(fullKey string, client *http.Client) {
|
||||
cache.mutex.Lock()
|
||||
defer cache.mutex.Unlock()
|
||||
cache.clients[fullKey] = client
|
||||
}
|
||||
|
||||
func (cache *proxyHTTPClientCache) resolveProxyKey(rawProxyURL string) string {
|
||||
if canonicalKey, ok := cache.aliases[rawProxyURL]; ok {
|
||||
return canonicalKey
|
||||
}
|
||||
return rawProxyURL
|
||||
}
|
||||
|
||||
func (cache *proxyHTTPClientCache) get(rawProxyURL string, policy HTTPTransportPolicy) (*http.Client, bool) {
|
||||
cache.mutex.RLock()
|
||||
defer cache.mutex.RUnlock()
|
||||
cacheKey := rawCacheKey
|
||||
if canonicalKey, ok := cache.aliases[rawCacheKey]; ok {
|
||||
cacheKey = canonicalKey
|
||||
}
|
||||
client, ok := cache.clients[cacheKey]
|
||||
proxyKey := cache.resolveProxyKey(rawProxyURL)
|
||||
client, ok := cache.clients[clientCacheKey(proxyKey, policy)]
|
||||
return client, ok
|
||||
}
|
||||
|
||||
func (cache *proxyHTTPClientCache) getOrCreate(rawCacheKey string, config *proxyURLConfig) (*http.Client, error) {
|
||||
func (cache *proxyHTTPClientCache) getOrCreate(
|
||||
rawProxyURL string,
|
||||
config *proxyURLConfig,
|
||||
policy HTTPTransportPolicy,
|
||||
factory func() (*http.Client, error),
|
||||
) (*http.Client, error) {
|
||||
cache.mutex.Lock()
|
||||
defer cache.mutex.Unlock()
|
||||
if client, ok := cache.clients[config.cacheKey]; ok {
|
||||
cache.aliases[rawCacheKey] = config.cacheKey
|
||||
|
||||
proxyKey := ""
|
||||
if config != nil {
|
||||
proxyKey = config.cacheKey
|
||||
cache.aliases[rawProxyURL] = proxyKey
|
||||
} else if rawProxyURL != "" {
|
||||
proxyKey = cache.resolveProxyKey(rawProxyURL)
|
||||
}
|
||||
fullKey := clientCacheKey(proxyKey, policy)
|
||||
if client, ok := cache.clients[fullKey]; ok {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, err := newProxyHTTPClient(config.parsedURL)
|
||||
client, err := factory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache.clients[config.cacheKey] = client
|
||||
cache.aliases[rawCacheKey] = config.cacheKey
|
||||
cache.clients[fullKey] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cache *proxyHTTPClientCache) remove(cacheKey string) *http.Client {
|
||||
func (cache *proxyHTTPClientCache) removeProxy(proxyCacheKey string) []*http.Client {
|
||||
cache.mutex.Lock()
|
||||
defer cache.mutex.Unlock()
|
||||
client := cache.clients[cacheKey]
|
||||
delete(cache.clients, cacheKey)
|
||||
removed := make([]*http.Client, 0)
|
||||
prefix := proxyCacheKey + "\x00"
|
||||
for key, client := range cache.clients {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
removed = append(removed, client)
|
||||
delete(cache.clients, key)
|
||||
}
|
||||
}
|
||||
for alias, canonicalKey := range cache.aliases {
|
||||
if canonicalKey == cacheKey {
|
||||
if canonicalKey == proxyCacheKey {
|
||||
delete(cache.aliases, alias)
|
||||
}
|
||||
}
|
||||
return client
|
||||
return removed
|
||||
}
|
||||
|
||||
func (cache *proxyHTTPClientCache) reset() map[string]*http.Client {
|
||||
@@ -227,13 +261,11 @@ func (cache *proxyHTTPClientCache) reset() map[string]*http.Client {
|
||||
return oldClients
|
||||
}
|
||||
|
||||
func newProxyHTTPClient(proxyURL *url.URL) (*http.Client, error) {
|
||||
transport := newRelayHTTPTransport()
|
||||
|
||||
func configureProxyTransport(transport *http.Transport, proxyURL *url.URL) error {
|
||||
switch proxyURL.Scheme {
|
||||
case "http", "https":
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
|
||||
return nil
|
||||
case "socks5", "socks5h":
|
||||
transport.Proxy = nil
|
||||
forwardDialer := &net.Dialer{
|
||||
@@ -242,31 +274,104 @@ func newProxyHTTPClient(proxyURL *url.URL) (*http.Client, error) {
|
||||
}
|
||||
dialer, err := proxy.FromURL(proxyURL, forwardDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
contextDialer, ok := dialer.(proxy.ContextDialer)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("SOCKS proxy dialer does not support context cancellation")
|
||||
return fmt.Errorf("SOCKS proxy dialer does not support context cancellation")
|
||||
}
|
||||
transport.DialContext = contextDialer.DialContext
|
||||
|
||||
return nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy scheme")
|
||||
return fmt.Errorf("unsupported proxy scheme")
|
||||
}
|
||||
}
|
||||
|
||||
return newRelayHTTPClient(transport), nil
|
||||
func newTransportFactory(proxyURL *url.URL, tlsConfig *tls.Config) (func() *http.Transport, error) {
|
||||
// Validate proxy configuration once before creating shard transports.
|
||||
if proxyURL != nil {
|
||||
probe := newRelayHTTPTransport()
|
||||
if err := configureProxyTransport(probe, proxyURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return func() *http.Transport {
|
||||
transport := newRelayHTTPTransport()
|
||||
if proxyURL != nil {
|
||||
_ = configureProxyTransport(transport, proxyURL)
|
||||
} else {
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig.Clone()
|
||||
}
|
||||
return transport
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newHTTPClientFromPolicy(policy HTTPTransportPolicy, proxyURL *url.URL, tlsConfig *tls.Config) (*http.Client, error) {
|
||||
factory, err := newTransportFactory(proxyURL, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newHTTPClientFromTransportFactory(policy, factory), nil
|
||||
}
|
||||
|
||||
func newHTTPClientFromTransportFactory(policy HTTPTransportPolicy, factory func() *http.Transport) *http.Client {
|
||||
if policy.Shards < 1 {
|
||||
policy.Shards = 1
|
||||
}
|
||||
if policy.Protocol == dto.HTTPProtocolHTTP1 || policy.Shards == 1 {
|
||||
transport := factory()
|
||||
applyHTTPTransportPolicy(transport, policy)
|
||||
return newRelayHTTPClient(transport)
|
||||
}
|
||||
shardedFactory := func() *http.Transport {
|
||||
transport := factory()
|
||||
applyHTTPTransportPolicy(transport, policy)
|
||||
return transport
|
||||
}
|
||||
return newRelayHTTPClient(newShardedRoundTripper(policy, shardedFactory))
|
||||
}
|
||||
|
||||
func newDirectHTTPClient(policy HTTPTransportPolicy, tlsConfig *tls.Config) *http.Client {
|
||||
client, err := newHTTPClientFromPolicy(policy, nil, tlsConfig)
|
||||
if err != nil {
|
||||
// Direct clients cannot fail proxy configuration.
|
||||
transport := newRelayHTTPTransport()
|
||||
applyHTTPTransportPolicy(transport, policy)
|
||||
return newRelayHTTPClient(transport)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// newHTTPClientWithPolicyAndTLS is a test seam that builds a never-used transport
|
||||
// stack with the given policy and TLS config (for httptest certificate trust).
|
||||
func newHTTPClientWithPolicyAndTLS(policy HTTPTransportPolicy, tlsConfig *tls.Config) *http.Client {
|
||||
return newDirectHTTPClient(policy, tlsConfig)
|
||||
}
|
||||
|
||||
func newProxyHTTPClient(proxyURL *url.URL) (*http.Client, error) {
|
||||
return newHTTPClientFromPolicy(defaultHTTPTransportPolicy(), proxyURL, nil)
|
||||
}
|
||||
|
||||
// GetHttpClientWithProxy returns the default client or a cached proxy-enabled client.
|
||||
func GetHttpClientWithProxy(rawProxyURL string) (*http.Client, error) {
|
||||
return GetHttpClientWithProxySettings(rawProxyURL, dto.ChannelSettings{})
|
||||
}
|
||||
|
||||
// GetHttpClientWithProxySettings returns a cached HTTP client for the proxy URL and
|
||||
// channel transport settings. Default auto + 1 shard shares the same client pool as
|
||||
// GetHttpClientWithProxy / GetHttpClient for the empty-proxy case.
|
||||
func GetHttpClientWithProxySettings(rawProxyURL string, settings dto.ChannelSettings) (*http.Client, error) {
|
||||
policy := NormalizeHTTPTransportPolicy(settings)
|
||||
trimmedProxyURL := strings.TrimSpace(rawProxyURL)
|
||||
|
||||
if trimmedProxyURL == "" {
|
||||
if client := GetHttpClient(); client != nil {
|
||||
return client, nil
|
||||
}
|
||||
return http.DefaultClient, nil
|
||||
return getOrCreateDirectClient(policy)
|
||||
}
|
||||
if client, ok := proxyClients.get(trimmedProxyURL); ok {
|
||||
|
||||
if client, ok := proxyClients.get(trimmedProxyURL, policy); ok {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -278,10 +383,31 @@ func GetHttpClientWithProxy(rawProxyURL string) (*http.Client, error) {
|
||||
if legacySuffixStripped {
|
||||
warnLegacyProxyURLOnce(config)
|
||||
}
|
||||
return proxyClients.getOrCreate(trimmedProxyURL, config)
|
||||
return proxyClients.getOrCreate(trimmedProxyURL, config, policy, func() (*http.Client, error) {
|
||||
return newHTTPClientFromPolicy(policy, config.parsedURL, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// InvalidateProxyClient removes one proxy client and closes its idle connections.
|
||||
func getOrCreateDirectClient(policy HTTPTransportPolicy) (*http.Client, error) {
|
||||
defaultPolicy := defaultHTTPTransportPolicy()
|
||||
if policy == defaultPolicy {
|
||||
if client := GetHttpClient(); client != nil {
|
||||
return client, nil
|
||||
}
|
||||
// Compatibility with pre-init callers: never assign httpClient outside InitHttpClient.
|
||||
return http.DefaultClient, nil
|
||||
}
|
||||
|
||||
if client, ok := proxyClients.get("", policy); ok {
|
||||
return client, nil
|
||||
}
|
||||
return proxyClients.getOrCreate("", nil, policy, func() (*http.Client, error) {
|
||||
return newDirectHTTPClient(policy, nil), nil
|
||||
})
|
||||
}
|
||||
|
||||
// InvalidateProxyClient removes every cached policy variant for one proxy and
|
||||
// closes their idle connections (including all HTTP/2 shards).
|
||||
func InvalidateProxyClient(rawProxyURL string) {
|
||||
parsedURL, legacySuffixStripped, err := common.ParseProxyURLRuntime(rawProxyURL)
|
||||
if err != nil || parsedURL == nil {
|
||||
@@ -291,16 +417,26 @@ func InvalidateProxyClient(rawProxyURL string) {
|
||||
if legacySuffixStripped {
|
||||
warnLegacyProxyURLOnce(config)
|
||||
}
|
||||
if client := proxyClients.remove(config.cacheKey); client != nil {
|
||||
for _, client := range proxyClients.removeProxy(config.cacheKey) {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// ResetProxyClientCache clears all cached proxy clients.
|
||||
// ResetProxyClientCache clears cached proxy and non-default direct policy clients
|
||||
// and closes idle connections on every transport/shard. The package-level default
|
||||
// httpClient pointer stays stable after InitHttpClient; it is only closed and
|
||||
// re-registered in the policy cache so concurrent GetHttpClient readers never race
|
||||
// a pointer replacement.
|
||||
func ResetProxyClientCache() {
|
||||
defaultClient := httpClient
|
||||
for _, client := range proxyClients.reset() {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
if defaultClient == nil {
|
||||
return
|
||||
}
|
||||
defaultClient.CloseIdleConnections()
|
||||
proxyClients.store(clientCacheKey("", defaultHTTPTransportPolicy()), defaultClient)
|
||||
}
|
||||
|
||||
// NewProxyHttpClient is kept for compatibility.
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func withRelayHTTPTransportSettings(t *testing.T) {
|
||||
t.Helper()
|
||||
prevMaxIdle := common.RelayMaxIdleConns
|
||||
prevPerHost := common.RelayMaxIdleConnsPerHost
|
||||
prevTimeout := common.RelayIdleConnTimeout
|
||||
common.RelayMaxIdleConns = 500
|
||||
common.RelayMaxIdleConnsPerHost = 100
|
||||
common.RelayIdleConnTimeout = 90
|
||||
t.Cleanup(func() {
|
||||
common.RelayMaxIdleConns = prevMaxIdle
|
||||
common.RelayMaxIdleConnsPerHost = prevPerHost
|
||||
common.RelayIdleConnTimeout = prevTimeout
|
||||
})
|
||||
}
|
||||
|
||||
func initDefaultHTTPClientFixture(t *testing.T) *http.Client {
|
||||
t.Helper()
|
||||
withRelayHTTPTransportSettings(t)
|
||||
if httpClient == nil {
|
||||
InitHttpClient()
|
||||
} else {
|
||||
ResetProxyClientCache()
|
||||
}
|
||||
require.NotNil(t, httpClient)
|
||||
t.Cleanup(ResetProxyClientCache)
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func TestShardedRoundTripperPerOriginRotation(t *testing.T) {
|
||||
s := &shardedRoundTripper{n: 4}
|
||||
originA := "https://a.example:443"
|
||||
originB := "https://b.example:443"
|
||||
|
||||
gotA := make([]uint32, 0, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
gotA = append(gotA, s.pickShard(originA))
|
||||
}
|
||||
assert.Equal(t, []uint32{0, 1, 2, 3, 0, 1, 2, 3}, gotA)
|
||||
|
||||
gotB := make([]uint32, 0, 4)
|
||||
for i := 0; i < 4; i++ {
|
||||
gotB = append(gotB, s.pickShard(originB))
|
||||
}
|
||||
assert.Equal(t, []uint32{0, 1, 2, 3}, gotB, "independent origins must have independent counters")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const workers = 32
|
||||
const perWorker = 50
|
||||
var badShardCount atomic.Uint32
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < perWorker; j++ {
|
||||
idx := s.pickShard(originA)
|
||||
if idx >= 4 {
|
||||
badShardCount.Add(1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
assert.Equal(t, uint32(0), badShardCount.Load())
|
||||
}
|
||||
|
||||
func TestOriginKeyUsesSchemeAndHost(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "HTTPS://Example.COM:8443/path", nil)
|
||||
assert.Equal(t, "https://Example.COM:8443", originKey(req))
|
||||
}
|
||||
|
||||
func testTLSClientConfig(t *testing.T, server *httptest.Server) *tls.Config {
|
||||
t.Helper()
|
||||
pool := x509.NewCertPool()
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw})
|
||||
require.True(t, pool.AppendCertsFromPEM(certPEM))
|
||||
return &tls.Config{RootCAs: pool}
|
||||
}
|
||||
|
||||
func startHTTP2TLSServer(t *testing.T, handler http.Handler) *httptest.Server {
|
||||
t.Helper()
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
server.EnableHTTP2 = true
|
||||
server.StartTLS()
|
||||
t.Cleanup(server.Close)
|
||||
return server
|
||||
}
|
||||
|
||||
func drainClose(t *testing.T, resp *http.Response) {
|
||||
t.Helper()
|
||||
require.NotNil(t, resp)
|
||||
_, err := io.Copy(io.Discard, resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
}
|
||||
|
||||
func TestAutoOneShardNegotiatesHTTP2SingleConnection(t *testing.T) {
|
||||
withRelayHTTPTransportSettings(t)
|
||||
|
||||
var mu sync.Mutex
|
||||
addrs := make(map[string]struct{})
|
||||
var sawHTTP2 atomic.Bool
|
||||
|
||||
server := startHTTP2TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
addrs[r.RemoteAddr] = struct{}{}
|
||||
mu.Unlock()
|
||||
if r.ProtoMajor == 2 {
|
||||
sawHTTP2.Store(true)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
client := newHTTPClientWithPolicyAndTLS(defaultHTTPTransportPolicy(), testTLSClientConfig(t, server))
|
||||
for i := 0; i < 4; i++ {
|
||||
resp, err := client.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, resp.ProtoMajor)
|
||||
drainClose(t, resp)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
assert.True(t, sawHTTP2.Load())
|
||||
assert.Len(t, addrs, 1, "auto+1 must reuse a single HTTP/2 connection")
|
||||
}
|
||||
|
||||
func TestFourShardHTTP2ReusesExactlyFourConnections(t *testing.T) {
|
||||
withRelayHTTPTransportSettings(t)
|
||||
|
||||
var mu sync.Mutex
|
||||
addrs := make(map[string]struct{})
|
||||
var nonHTTP2Count atomic.Uint32
|
||||
|
||||
server := startHTTP2TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
addrs[r.RemoteAddr] = struct{}{}
|
||||
mu.Unlock()
|
||||
if r.ProtoMajor != 2 {
|
||||
nonHTTP2Count.Add(1)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
policy := HTTPTransportPolicy{Protocol: dto.HTTPProtocolAuto, Shards: 4}
|
||||
client := newHTTPClientWithPolicyAndTLS(policy, testTLSClientConfig(t, server))
|
||||
for i := 0; i < 8; i++ {
|
||||
resp, err := client.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, resp.ProtoMajor)
|
||||
drainClose(t, resp)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
assert.Equal(t, uint32(0), nonHTTP2Count.Load())
|
||||
assert.Len(t, addrs, 4, "four shards must establish and reuse exactly four connections")
|
||||
}
|
||||
|
||||
func TestForcedHTTP1AgainstHTTP2Server(t *testing.T) {
|
||||
withRelayHTTPTransportSettings(t)
|
||||
|
||||
var nonHTTP1Count atomic.Uint32
|
||||
server := startHTTP2TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ProtoMajor != 1 {
|
||||
nonHTTP1Count.Add(1)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
policy := HTTPTransportPolicy{Protocol: dto.HTTPProtocolHTTP1, Shards: 1}
|
||||
client := newHTTPClientWithPolicyAndTLS(policy, testTLSClientConfig(t, server))
|
||||
resp, err := client.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, resp.ProtoMajor)
|
||||
drainClose(t, resp)
|
||||
assert.Equal(t, uint32(0), nonHTTP1Count.Load())
|
||||
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
assert.False(t, transport.DisableKeepAlives)
|
||||
assert.False(t, transport.ForceAttemptHTTP2)
|
||||
assert.NotNil(t, transport.TLSNextProto)
|
||||
assert.Len(t, transport.TLSNextProto, 0)
|
||||
}
|
||||
|
||||
func TestForcedHTTP1ConcurrentDistinctConnections(t *testing.T) {
|
||||
withRelayHTTPTransportSettings(t)
|
||||
|
||||
const k = 8
|
||||
var mu sync.Mutex
|
||||
addrs := make(map[string]struct{})
|
||||
arrived := make(chan struct{}, k)
|
||||
release := make(chan struct{})
|
||||
var nonHTTP1Count atomic.Uint32
|
||||
|
||||
server := startHTTP2TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
addrs[r.RemoteAddr] = struct{}{}
|
||||
mu.Unlock()
|
||||
if r.ProtoMajor != 1 {
|
||||
nonHTTP1Count.Add(1)
|
||||
}
|
||||
arrived <- struct{}{}
|
||||
<-release
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
policy := HTTPTransportPolicy{Protocol: dto.HTTPProtocolHTTP1, Shards: 1}
|
||||
client := newHTTPClientWithPolicyAndTLS(policy, testTLSClientConfig(t, server))
|
||||
|
||||
errCh := make(chan error, k)
|
||||
for i := 0; i < k; i++ {
|
||||
go func() {
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if resp.ProtoMajor != 1 {
|
||||
errCh <- fmt.Errorf("expected HTTP/1.x, got %s", resp.Proto)
|
||||
_ = resp.Body.Close()
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
errCh <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < k; i++ {
|
||||
<-arrived
|
||||
}
|
||||
mu.Lock()
|
||||
activeAddrs := len(addrs)
|
||||
mu.Unlock()
|
||||
close(release)
|
||||
|
||||
for i := 0; i < k; i++ {
|
||||
require.NoError(t, <-errCh)
|
||||
}
|
||||
assert.Equal(t, uint32(0), nonHTTP1Count.Load())
|
||||
assert.Equal(t, k, activeAddrs, "all HTTP/1.1 handlers active together must use K distinct connections")
|
||||
}
|
||||
|
||||
func TestHTTPClientCachePolicyAndCompatibility(t *testing.T) {
|
||||
defaultClient := initDefaultHTTPClientFixture(t)
|
||||
|
||||
compat, err := GetHttpClientWithProxy("")
|
||||
require.NoError(t, err)
|
||||
aware, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{})
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, defaultClient, compat)
|
||||
assert.Same(t, compat, aware)
|
||||
assert.Same(t, GetHttpClient(), aware)
|
||||
|
||||
http1, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{HTTPProtocol: dto.HTTPProtocolHTTP1})
|
||||
require.NoError(t, err)
|
||||
assert.NotSame(t, aware, http1)
|
||||
|
||||
sharded, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{HTTP2ConnectionShards: 4})
|
||||
require.NoError(t, err)
|
||||
assert.NotSame(t, aware, sharded)
|
||||
assert.NotSame(t, http1, sharded)
|
||||
|
||||
proxyA := "http://proxy.example:8080"
|
||||
proxyAlias := "http://proxy.example:8080/"
|
||||
clientA, err := GetHttpClientWithProxy(proxyA)
|
||||
require.NoError(t, err)
|
||||
clientAlias, err := GetHttpClientWithProxy(proxyAlias)
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, clientA, clientAlias, "canonical proxy aliases must share the default policy client")
|
||||
|
||||
proxyHTTP1, err := GetHttpClientWithProxySettings(proxyA, dto.ChannelSettings{HTTPProtocol: dto.HTTPProtocolHTTP1})
|
||||
require.NoError(t, err)
|
||||
assert.NotSame(t, clientA, proxyHTTP1)
|
||||
}
|
||||
|
||||
func TestHTTPClientCacheConcurrentGetOrCreate(t *testing.T) {
|
||||
initDefaultHTTPClientFixture(t)
|
||||
|
||||
proxyURL := "http://concurrent-proxy.example:9090"
|
||||
const workers = 32
|
||||
results := make([]*http.Client, workers)
|
||||
errs := make([]error, workers)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
i := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
client, err := GetHttpClientWithProxySettings(proxyURL, dto.ChannelSettings{HTTP2ConnectionShards: 3})
|
||||
errs[i] = err
|
||||
results[i] = client
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 0; i < workers; i++ {
|
||||
require.NoError(t, errs[i])
|
||||
}
|
||||
for i := 1; i < workers; i++ {
|
||||
assert.Same(t, results[0], results[i])
|
||||
}
|
||||
}
|
||||
|
||||
type closeCountingRoundTripper struct {
|
||||
closes atomic.Int32
|
||||
}
|
||||
|
||||
func (c *closeCountingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: http.NoBody,
|
||||
Header: make(http.Header),
|
||||
Request: req,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *closeCountingRoundTripper) CloseIdleConnections() {
|
||||
c.closes.Add(1)
|
||||
}
|
||||
|
||||
func TestShardedRoundTripperCloseIdleConnectionsFansOut(t *testing.T) {
|
||||
trackers := []*closeCountingRoundTripper{{}, {}, {}}
|
||||
shards := make([]http.RoundTripper, len(trackers))
|
||||
for i, tracker := range trackers {
|
||||
shards[i] = tracker
|
||||
}
|
||||
s := &shardedRoundTripper{shards: shards, n: uint32(len(shards))}
|
||||
s.CloseIdleConnections()
|
||||
for _, tracker := range trackers {
|
||||
assert.Equal(t, int32(1), tracker.closes.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateProxyClientClosesAllPolicyVariants(t *testing.T) {
|
||||
initDefaultHTTPClientFixture(t)
|
||||
|
||||
proxyURL := "http://invalidate-proxy.example:8080"
|
||||
defaultClient, err := GetHttpClientWithProxy(proxyURL)
|
||||
require.NoError(t, err)
|
||||
http1Client, err := GetHttpClientWithProxySettings(proxyURL, dto.ChannelSettings{HTTPProtocol: dto.HTTPProtocolHTTP1})
|
||||
require.NoError(t, err)
|
||||
shardedClient, err := GetHttpClientWithProxySettings(proxyURL, dto.ChannelSettings{HTTP2ConnectionShards: 2})
|
||||
require.NoError(t, err)
|
||||
|
||||
InvalidateProxyClient(proxyURL)
|
||||
|
||||
afterDefault, err := GetHttpClientWithProxy(proxyURL)
|
||||
require.NoError(t, err)
|
||||
afterHTTP1, err := GetHttpClientWithProxySettings(proxyURL, dto.ChannelSettings{HTTPProtocol: dto.HTTPProtocolHTTP1})
|
||||
require.NoError(t, err)
|
||||
afterSharded, err := GetHttpClientWithProxySettings(proxyURL, dto.ChannelSettings{HTTP2ConnectionShards: 2})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotSame(t, defaultClient, afterDefault)
|
||||
assert.NotSame(t, http1Client, afterHTTP1)
|
||||
assert.NotSame(t, shardedClient, afterSharded)
|
||||
}
|
||||
|
||||
func TestResetProxyClientCacheKeepsDefaultPointerAndRecreatesVariants(t *testing.T) {
|
||||
defaultClient := initDefaultHTTPClientFixture(t)
|
||||
|
||||
http1Client, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{HTTPProtocol: dto.HTTPProtocolHTTP1})
|
||||
require.NoError(t, err)
|
||||
shardedClient, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{HTTP2ConnectionShards: 3})
|
||||
require.NoError(t, err)
|
||||
proxyClient, err := GetHttpClientWithProxy("http://reset-proxy.example:8080")
|
||||
require.NoError(t, err)
|
||||
|
||||
ResetProxyClientCache()
|
||||
|
||||
assert.Same(t, defaultClient, GetHttpClient(), "default httpClient pointer must stay stable across reset")
|
||||
aware, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{})
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, defaultClient, aware)
|
||||
|
||||
afterHTTP1, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{HTTPProtocol: dto.HTTPProtocolHTTP1})
|
||||
require.NoError(t, err)
|
||||
afterSharded, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{HTTP2ConnectionShards: 3})
|
||||
require.NoError(t, err)
|
||||
afterProxy, err := GetHttpClientWithProxy("http://reset-proxy.example:8080")
|
||||
require.NoError(t, err)
|
||||
assert.NotSame(t, http1Client, afterHTTP1)
|
||||
assert.NotSame(t, shardedClient, afterSharded)
|
||||
assert.NotSame(t, proxyClient, afterProxy)
|
||||
}
|
||||
|
||||
func TestResetProxyClientCacheClosesDefaultIdlePool(t *testing.T) {
|
||||
defaultClient := initDefaultHTTPClientFixture(t)
|
||||
tracker := &closeCountingRoundTripper{}
|
||||
previousTransport := defaultClient.Transport
|
||||
defaultClient.Transport = tracker
|
||||
t.Cleanup(func() {
|
||||
defaultClient.Transport = previousTransport
|
||||
})
|
||||
|
||||
ResetProxyClientCache()
|
||||
|
||||
assert.Same(t, defaultClient, GetHttpClient())
|
||||
assert.GreaterOrEqual(t, tracker.closes.Load(), int32(1), "reset must close idle connections on the stable default client")
|
||||
aware, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{})
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, defaultClient, aware)
|
||||
}
|
||||
|
||||
func TestResetProxyClientCacheConcurrentWithGetHttpClient(t *testing.T) {
|
||||
initDefaultHTTPClientFixture(t)
|
||||
|
||||
const workers = 64
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(workers * 2)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = GetHttpClient()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ResetProxyClientCache()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
assert.NotNil(t, GetHttpClient())
|
||||
aware, err := GetHttpClientWithProxySettings("", dto.ChannelSettings{})
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, GetHttpClient(), aware)
|
||||
}
|
||||
|
||||
func TestCloseIdleConnectionsRedialsHTTP2(t *testing.T) {
|
||||
withRelayHTTPTransportSettings(t)
|
||||
|
||||
var mu sync.Mutex
|
||||
addrs := make([]string, 0, 2)
|
||||
|
||||
server := startHTTP2TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
addrs = append(addrs, r.RemoteAddr)
|
||||
mu.Unlock()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
client := newHTTPClientWithPolicyAndTLS(defaultHTTPTransportPolicy(), testTLSClientConfig(t, server))
|
||||
resp, err := client.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
drainClose(t, resp)
|
||||
|
||||
client.CloseIdleConnections()
|
||||
|
||||
resp, err = client.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
drainClose(t, resp)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.Len(t, addrs, 2)
|
||||
assert.NotEqual(t, addrs[0], addrs[1], "after CloseIdleConnections the next request must redial")
|
||||
}
|
||||
|
||||
func TestNormalizeHTTPTransportPolicyClampsWithoutPanic(t *testing.T) {
|
||||
assert.Equal(t, defaultHTTPTransportPolicy(), NormalizeHTTPTransportPolicy(dto.ChannelSettings{}))
|
||||
assert.Equal(t, HTTPTransportPolicy{Protocol: dto.HTTPProtocolAuto, Shards: 1}, NormalizeHTTPTransportPolicy(dto.ChannelSettings{HTTPProtocol: "AUTO"}))
|
||||
assert.Equal(t, HTTPTransportPolicy{Protocol: dto.HTTPProtocolHTTP1, Shards: 1}, NormalizeHTTPTransportPolicy(dto.ChannelSettings{HTTPProtocol: "HTTP1", HTTP2ConnectionShards: 8}))
|
||||
assert.Equal(t, HTTPTransportPolicy{Protocol: dto.HTTPProtocolAuto, Shards: 1}, NormalizeHTTPTransportPolicy(dto.ChannelSettings{HTTPProtocol: "http3"}))
|
||||
assert.Equal(t, HTTPTransportPolicy{Protocol: dto.HTTPProtocolAuto, Shards: 1}, NormalizeHTTPTransportPolicy(dto.ChannelSettings{HTTP2ConnectionShards: -3}))
|
||||
assert.Equal(t, HTTPTransportPolicy{Protocol: dto.HTTPProtocolAuto, Shards: 8}, NormalizeHTTPTransportPolicy(dto.ChannelSettings{HTTP2ConnectionShards: 99}))
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
)
|
||||
|
||||
// HTTPTransportPolicy is the runtime-normalized outbound HTTP transport policy
|
||||
// for a channel. Unknown or out-of-range stored values are clamped safely.
|
||||
type HTTPTransportPolicy struct {
|
||||
Protocol string // dto.HTTPProtocolAuto or dto.HTTPProtocolHTTP1
|
||||
Shards int // 1..dto.MaxHTTP2ConnectionShards
|
||||
}
|
||||
|
||||
var httpTransportPolicyWarnings sync.Map
|
||||
|
||||
func defaultHTTPTransportPolicy() HTTPTransportPolicy {
|
||||
return HTTPTransportPolicy{
|
||||
Protocol: dto.HTTPProtocolAuto,
|
||||
Shards: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeHTTPTransportPolicy converts channel settings into a safe runtime policy.
|
||||
// Invalid stored values never panic; they clamp to defaults and warn once per bad value.
|
||||
func NormalizeHTTPTransportPolicy(settings dto.ChannelSettings) HTTPTransportPolicy {
|
||||
policy := defaultHTTPTransportPolicy()
|
||||
|
||||
protocol := strings.ToLower(strings.TrimSpace(settings.HTTPProtocol))
|
||||
switch protocol {
|
||||
case "", dto.HTTPProtocolAuto:
|
||||
policy.Protocol = dto.HTTPProtocolAuto
|
||||
case dto.HTTPProtocolHTTP1:
|
||||
policy.Protocol = dto.HTTPProtocolHTTP1
|
||||
default:
|
||||
warnHTTPTransportPolicyOnce("http_protocol", settings.HTTPProtocol)
|
||||
policy.Protocol = dto.HTTPProtocolAuto
|
||||
}
|
||||
|
||||
shards := settings.HTTP2ConnectionShards
|
||||
switch {
|
||||
case shards == 0:
|
||||
policy.Shards = 1
|
||||
case shards < 1:
|
||||
warnHTTPTransportPolicyOnce("http2_connection_shards", fmt.Sprintf("%d", shards))
|
||||
policy.Shards = 1
|
||||
case shards > dto.MaxHTTP2ConnectionShards:
|
||||
warnHTTPTransportPolicyOnce("http2_connection_shards", fmt.Sprintf("%d", shards))
|
||||
policy.Shards = dto.MaxHTTP2ConnectionShards
|
||||
default:
|
||||
policy.Shards = shards
|
||||
}
|
||||
|
||||
if policy.Protocol == dto.HTTPProtocolHTTP1 {
|
||||
if settings.HTTP2ConnectionShards > 1 {
|
||||
warnHTTPTransportPolicyOnce(
|
||||
"http_protocol+http2_connection_shards",
|
||||
fmt.Sprintf("%s+%d", dto.HTTPProtocolHTTP1, settings.HTTP2ConnectionShards),
|
||||
)
|
||||
}
|
||||
policy.Shards = 1
|
||||
}
|
||||
if policy.Shards < 1 {
|
||||
policy.Shards = 1
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
func warnHTTPTransportPolicyOnce(field, value string) {
|
||||
key := field + "=" + value
|
||||
if _, loaded := httpTransportPolicyWarnings.LoadOrStore(key, struct{}{}); loaded {
|
||||
return
|
||||
}
|
||||
logger.LogWarn(
|
||||
context.Background(),
|
||||
fmt.Sprintf("invalid channel http transport setting clamped: %s=%q", field, value),
|
||||
)
|
||||
}
|
||||
|
||||
func (p HTTPTransportPolicy) cacheKeyPart() string {
|
||||
return fmt.Sprintf("%s|%d", p.Protocol, p.Shards)
|
||||
}
|
||||
|
||||
func (p HTTPTransportPolicy) String() string {
|
||||
return p.cacheKeyPart()
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
)
|
||||
|
||||
// shardedRoundTripper fans requests for each origin across N independent
|
||||
// transports so each origin can keep N reusable HTTP/2 connections.
|
||||
type shardedRoundTripper struct {
|
||||
shards []http.RoundTripper
|
||||
n uint32
|
||||
policy HTTPTransportPolicy
|
||||
counters sync.Map // origin -> *atomic.Uint32
|
||||
}
|
||||
|
||||
func newShardedRoundTripper(policy HTTPTransportPolicy, factory func() *http.Transport) *shardedRoundTripper {
|
||||
n := policy.Shards
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
shards := make([]http.RoundTripper, n)
|
||||
for i := 0; i < n; i++ {
|
||||
transport := factory()
|
||||
transport.MaxIdleConns = max(1, transport.MaxIdleConns/n)
|
||||
transport.MaxIdleConnsPerHost = max(1, transport.MaxIdleConnsPerHost/n)
|
||||
shards[i] = transport
|
||||
}
|
||||
return &shardedRoundTripper{
|
||||
shards: shards,
|
||||
n: uint32(n),
|
||||
policy: policy,
|
||||
}
|
||||
}
|
||||
|
||||
func originKey(req *http.Request) string {
|
||||
if req == nil || req.URL == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(req.URL.Scheme) + "://" + req.URL.Host
|
||||
}
|
||||
|
||||
func (s *shardedRoundTripper) pickShard(origin string) uint32 {
|
||||
if s.n <= 1 {
|
||||
return 0
|
||||
}
|
||||
counterAny, _ := s.counters.LoadOrStore(origin, &atomic.Uint32{})
|
||||
counter := counterAny.(*atomic.Uint32)
|
||||
return (counter.Add(1) - 1) % s.n
|
||||
}
|
||||
|
||||
func (s *shardedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
origin := originKey(req)
|
||||
idx := s.pickShard(origin)
|
||||
resp, err := s.shards[idx].RoundTrip(req)
|
||||
if common.DebugEnabled {
|
||||
proto := ""
|
||||
if resp != nil {
|
||||
proto = resp.Proto
|
||||
}
|
||||
host := ""
|
||||
if req != nil && req.URL != nil {
|
||||
host = req.URL.Host
|
||||
}
|
||||
logger.LogDebug(
|
||||
req.Context(),
|
||||
fmt.Sprintf(
|
||||
"http transport: host=%s protocol=%s shard=%d/%d policy=%s negotiated=%s",
|
||||
host,
|
||||
s.policy.Protocol,
|
||||
idx,
|
||||
s.n,
|
||||
s.policy.cacheKeyPart(),
|
||||
proto,
|
||||
),
|
||||
)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (s *shardedRoundTripper) CloseIdleConnections() {
|
||||
for _, shard := range s.shards {
|
||||
closeIdleConnections(shard)
|
||||
}
|
||||
}
|
||||
|
||||
func closeIdleConnections(rt http.RoundTripper) {
|
||||
type idleCloser interface {
|
||||
CloseIdleConnections()
|
||||
}
|
||||
if closer, ok := rt.(idleCloser); ok {
|
||||
closer.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// applyHTTP1Force disables automatic HTTP/2 on a never-used transport.
|
||||
// ForceAttemptHTTP2=false alone is insufficient; a non-nil empty TLSNextProto
|
||||
// map prevents net/http from wiring HTTP/2.
|
||||
func applyHTTP1Force(transport *http.Transport) {
|
||||
if transport == nil {
|
||||
return
|
||||
}
|
||||
transport.ForceAttemptHTTP2 = false
|
||||
transport.DisableKeepAlives = false
|
||||
transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
||||
if transport.TLSClientConfig != nil {
|
||||
cfg := transport.TLSClientConfig.Clone()
|
||||
cfg.NextProtos = nil
|
||||
transport.TLSClientConfig = cfg
|
||||
}
|
||||
}
|
||||
|
||||
func applyHTTPTransportPolicy(transport *http.Transport, policy HTTPTransportPolicy) {
|
||||
if transport == nil {
|
||||
return
|
||||
}
|
||||
if policy.Protocol == dto.HTTPProtocolHTTP1 {
|
||||
applyHTTP1Force(transport)
|
||||
return
|
||||
}
|
||||
transport.ForceAttemptHTTP2 = true
|
||||
transport.DisableKeepAlives = false
|
||||
}
|
||||
Reference in New Issue
Block a user