From 4e1009f44380016ac290efd48592bb4abe756a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=88=90=E5=96=86?= Date: Fri, 13 Mar 2026 15:03:15 +0800 Subject: [PATCH 1/2] fix: use per-account proxy_url for OAuth token refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClaudeExecutor.Refresh() only read the global proxy-url from config.yaml when creating the HTTP client for token refresh. When proxy-url was empty (relying on HTTPS_PROXY env var or per-account proxy_url in credential files), the custom utls transport would attempt direct connections since it does not read environment variables. This caused silent token refresh failures in deployments that depend on proxy routing (e.g. VPS → residential IP) — the token would expire and auto-refresh would silently fail because errors were logged at DEBUG level. Changes: - ClaudeExecutor.Refresh(): always prefer auth.ProxyURL over cfg.ProxyURL, matching the priority in proxy_helpers.go (auth > cfg > env) - conductor.refreshAuth(): log refresh failures at WARN level instead of DEBUG, making them visible without enabling debug mode - config.example.yaml: document proxy resolution priority and the caveat that OAuth refresh does not read HTTPS_PROXY env vars Co-Authored-By: Claude Opus 4.6 --- config.example.yaml | 13 ++++++++++++- internal/runtime/executor/claude_executor.go | 10 +++++++++- sdk/cliproxy/auth/conductor.go | 6 +++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 40bb87210a..291ae38135 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -62,7 +62,18 @@ error-logs-max-files: 10 # When false, disable in-memory usage statistics aggregation usage-statistics-enabled: false -# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/ +# Global proxy URL for all outgoing connections (API requests + OAuth token refresh). +# Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080 +# +# Proxy resolution priority (highest to lowest): +# 1. Per-account proxy_url in auth credential JSON file (e.g. claude-user@example.com.json) +# 2. This global proxy-url +# 3. HTTPS_PROXY / HTTP_PROXY environment variables (standard http.Transport only) +# +# IMPORTANT: OAuth token refresh uses a custom TLS transport that does NOT read +# HTTPS_PROXY environment variables. If you rely on env vars for proxy routing, +# you must ALSO set this field or add "proxy_url" to your credential JSON files, +# otherwise token refresh requests will attempt direct connections. proxy-url: "" # When true, unprefixed model requests only use credentials without a prefix (except when prefix == model name). diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 7d0ddcf2d2..e3950f74d3 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -578,7 +578,15 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) ( if refreshToken == "" { return auth, nil } - svc := claudeauth.NewClaudeAuth(e.cfg) + // Use per-account proxy_url for token refresh, matching the priority in + // proxy_helpers.go: auth.ProxyURL > cfg.ProxyURL > env vars. + cfg := e.cfg + if auth.ProxyURL != "" { + cfgCopy := *cfg + cfgCopy.SDKConfig.ProxyURL = auth.ProxyURL + cfg = &cfgCopy + } + svc := claudeauth.NewClaudeAuth(cfg) td, err := svc.RefreshTokens(ctx, refreshToken) if err != nil { return nil, err diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index ae5b745c98..abb220a605 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -2178,8 +2178,12 @@ func (m *Manager) refreshAuth(ctx context.Context, id string) { log.Debugf("refresh canceled for %s, %s", auth.Provider, auth.ID) return } - log.Debugf("refreshed %s, %s, %v", auth.Provider, auth.ID, err) now := time.Now() + if err != nil { + log.Warnf("auth refresh failed for %s/%s: %v", auth.Provider, auth.ID, err) + } else { + log.Debugf("auth refresh ok for %s/%s", auth.Provider, auth.ID) + } if err != nil { m.mu.Lock() if current := m.auths[id]; current != nil { From 3ebef457731f068e69692f88c25a6a8b142cdb4a Mon Sep 17 00:00:00 2001 From: leecz Date: Sat, 14 Mar 2026 07:52:21 +0800 Subject: [PATCH 2/2] fix: unify proxy resolution for OAuth refresh with request path Extract ResolveProxyURL() in proxy_helpers.go as the single source of truth for proxy priority (auth > config > env). Both the request path (newProxyAwareHTTPClient) and the refresh path (claude_executor.Refresh) now call this shared function, eliminating the config-copy approach that could diverge from the documented priority. NewClaudeAuth gains an optional proxyURL parameter so callers can pass the pre-resolved URL without mutating the config struct. Addresses review feedback from @luispater on #1947. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/auth/claude/anthropic_auth.go | 17 +++++++++--- internal/runtime/executor/claude_executor.go | 9 ++---- internal/runtime/executor/proxy_helpers.go | 29 +++++++++++++------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/internal/auth/claude/anthropic_auth.go b/internal/auth/claude/anthropic_auth.go index 2853e418e6..21504b53b7 100644 --- a/internal/auth/claude/anthropic_auth.go +++ b/internal/auth/claude/anthropic_auth.go @@ -53,16 +53,25 @@ type ClaudeAuth struct { // It initializes the HTTP client with a custom TLS transport that uses Firefox // fingerprint to bypass Cloudflare's TLS fingerprinting on Anthropic domains. // +// An optional proxyURL may be provided to override the proxy in cfg.SDKConfig. +// This allows callers to pass a pre-resolved proxy URL (e.g. from ResolveProxyURL) +// so that both request and refresh paths share the same proxy priority logic. +// // Parameters: // - cfg: The application configuration containing proxy settings +// - proxyURL: Optional pre-resolved proxy URL that overrides cfg.SDKConfig.ProxyURL // // Returns: // - *ClaudeAuth: A new Claude authentication service instance -func NewClaudeAuth(cfg *config.Config) *ClaudeAuth { - // Use custom HTTP client with Firefox TLS fingerprint to bypass - // Cloudflare's bot detection on Anthropic domains +func NewClaudeAuth(cfg *config.Config, proxyURL ...string) *ClaudeAuth { + sdkCfg := &cfg.SDKConfig + if len(proxyURL) > 0 && proxyURL[0] != "" { + cfgCopy := cfg.SDKConfig + cfgCopy.ProxyURL = proxyURL[0] + sdkCfg = &cfgCopy + } return &ClaudeAuth{ - httpClient: NewAnthropicHttpClient(&cfg.SDKConfig), + httpClient: NewAnthropicHttpClient(sdkCfg), } } diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index e3950f74d3..4a5e8272f2 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -580,13 +580,8 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) ( } // Use per-account proxy_url for token refresh, matching the priority in // proxy_helpers.go: auth.ProxyURL > cfg.ProxyURL > env vars. - cfg := e.cfg - if auth.ProxyURL != "" { - cfgCopy := *cfg - cfgCopy.SDKConfig.ProxyURL = auth.ProxyURL - cfg = &cfgCopy - } - svc := claudeauth.NewClaudeAuth(cfg) + proxyURL := ResolveProxyURL(e.cfg, auth) + svc := claudeauth.NewClaudeAuth(e.cfg, proxyURL) td, err := svc.RefreshTokens(ctx, refreshToken) if err != nil { return nil, err diff --git a/internal/runtime/executor/proxy_helpers.go b/internal/runtime/executor/proxy_helpers.go index ab0f626acc..0e8d2cca7a 100644 --- a/internal/runtime/executor/proxy_helpers.go +++ b/internal/runtime/executor/proxy_helpers.go @@ -14,6 +14,24 @@ import ( "golang.org/x/net/proxy" ) +// ResolveProxyURL returns the effective proxy URL following the standard priority: +// 1. auth.ProxyURL (per-account override) +// 2. cfg.ProxyURL (global config) +// 3. "" (empty — caller decides fallback behavior) +func ResolveProxyURL(cfg *config.Config, auth *cliproxyauth.Auth) string { + if auth != nil { + if u := strings.TrimSpace(auth.ProxyURL); u != "" { + return u + } + } + if cfg != nil { + if u := strings.TrimSpace(cfg.ProxyURL); u != "" { + return u + } + } + return "" +} + // 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 @@ -33,16 +51,7 @@ func newProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip httpClient.Timeout = timeout } - // Priority 1: Use auth.ProxyURL if configured - var proxyURL string - if auth != nil { - proxyURL = strings.TrimSpace(auth.ProxyURL) - } - - // Priority 2: Use cfg.ProxyURL if auth proxy is not configured - if proxyURL == "" && cfg != nil { - proxyURL = strings.TrimSpace(cfg.ProxyURL) - } + proxyURL := ResolveProxyURL(cfg, auth) // If we have a proxy URL configured, set up the transport if proxyURL != "" {