refactor(runtime): enhance NewUtlsHTTPClient with context-based RoundTripper

- Updated `NewUtlsHTTPClient` to support context-aware RoundTrippers for protected hosts (e.g., Cloudflare bypass).
- Replaced `anthropicHosts` with `utlsProtectedHosts` to generalize host handling logic.
- Added unit test to validate context-based RoundTripper behavior.
- Replaced `NewProxyAwareHTTPClient` with `NewUtlsHTTPClient` in relevant executors for improved TLS fingerprinting.

Closes: #3680
This commit is contained in:
Luis Pater
2026-06-03 06:58:26 +08:00
parent 0e3c809ceb
commit 35ab084fc3
4 changed files with 73 additions and 23 deletions

View File

@@ -156,7 +156,7 @@ func (e *ClaudeExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err
}
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq)
}
@@ -260,7 +260,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
AuthValue: authValue,
})
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
if err != nil {
@@ -437,7 +437,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
AuthValue: authValue,
})
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
if err != nil {
@@ -674,7 +674,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
AuthValue: authValue,
})
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
resp, err := httpClient.Do(httpReq)
if err != nil {
helps.RecordAPIResponseError(ctx, e.cfg, err)

View File

@@ -744,7 +744,7 @@ func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err
}
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq)
}
@@ -821,7 +821,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
AuthType: authType,
AuthValue: authValue,
})
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
if err != nil {
@@ -987,7 +987,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
AuthType: authType,
AuthValue: authValue,
})
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
if err != nil {
@@ -1097,7 +1097,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
AuthValue: authValue,
})
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
if err != nil {

View File

@@ -1,6 +1,7 @@
package helps
import (
"context"
"net"
"net/http"
"strings"
@@ -128,21 +129,23 @@ func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
return resp, nil
}
// anthropicHosts contains the hosts that should use utls Chrome TLS fingerprint.
var anthropicHosts = map[string]struct{}{
// utlsProtectedHosts contains the hosts that should use utls Chrome TLS fingerprint
// to bypass Cloudflare's TLS fingerprinting.
var utlsProtectedHosts = map[string]struct{}{
"api.anthropic.com": {},
"chatgpt.com": {},
}
// fallbackRoundTripper uses utls for Anthropic HTTPS hosts and falls back to
// standard transport for all other requests (non-HTTPS or non-Anthropic hosts).
// fallbackRoundTripper uses utls for protected HTTPS hosts and falls back to
// standard transport for all other requests.
type fallbackRoundTripper struct {
utls *utlsRoundTripper
utls http.RoundTripper
fallback http.RoundTripper
}
func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Scheme == "https" {
if _, ok := anthropicHosts[strings.ToLower(req.URL.Hostname())]; ok {
if _, ok := utlsProtectedHosts[strings.ToLower(req.URL.Hostname())]; ok {
return f.utls.RoundTrip(req)
}
}
@@ -150,9 +153,9 @@ func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
}
// NewUtlsHTTPClient creates an HTTP client using utls Chrome TLS fingerprint.
// Use this for Claude API requests to match real Claude Code's TLS behavior.
// Use this for provider requests that need a Chrome-like TLS fingerprint.
// Falls back to standard transport for non-HTTPS requests.
func NewUtlsHTTPClient(cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
func NewUtlsHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
var proxyURL string
if auth != nil {
proxyURL = strings.TrimSpace(auth.ProxyURL)
@@ -161,18 +164,20 @@ func NewUtlsHTTPClient(cfg *config.Config, auth *cliproxyauth.Auth, timeout time
proxyURL = strings.TrimSpace(cfg.ProxyURL)
}
utlsRT := newUtlsRoundTripper(proxyURL)
var standardTransport http.RoundTripper = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
var ctxRoundTripper http.RoundTripper
if ctx != nil {
ctxRoundTripper, _ = ctx.Value("cliproxy.roundtripper").(http.RoundTripper)
}
var utlsRT http.RoundTripper = newUtlsRoundTripper(proxyURL)
var standardTransport http.RoundTripper = http.DefaultTransport
if proxyURL != "" {
if transport := buildProxyTransport(proxyURL); transport != nil {
standardTransport = transport
}
} else if ctxRoundTripper != nil {
utlsRT = ctxRoundTripper
standardTransport = ctxRoundTripper
}
client := &http.Client{

View File

@@ -0,0 +1,45 @@
package helps
import (
"context"
"io"
"net/http"
"strings"
"testing"
)
type utlsClientRoundTripFunc func(*http.Request) (*http.Response, error)
func (f utlsClientRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func TestNewUtlsHTTPClientUsesContextRoundTripperForProtectedHost(t *testing.T) {
t.Parallel()
called := false
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", utlsClientRoundTripFunc(func(req *http.Request) (*http.Response, error) {
called = true
if req.URL.Hostname() != "chatgpt.com" {
t.Fatalf("hostname = %q, want chatgpt.com", req.URL.Hostname())
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("{}")),
Request: req,
}, nil
}))
client := NewUtlsHTTPClient(ctx, nil, nil, 0)
resp, err := client.Get("https://chatgpt.com/backend-api/codex/responses")
if err != nil {
t.Fatalf("client.Get returned error: %v", err)
}
if errClose := resp.Body.Close(); errClose != nil {
t.Fatalf("response body close returned error: %v", errClose)
}
if !called {
t.Fatal("expected context RoundTripper to handle protected host request")
}
}