refactor(proxy): route requests through hyper with proxy-aware forwarding

Rework forwarder request dispatch to always prefer the hyper raw write
path (header case preservation) over reqwest:

Request routing:
- HTTP/HTTPS proxy: hyper raw write through CONNECT tunnel (case preserved)
- SOCKS5 proxy: reqwest fallback (CONNECT not supported for SOCKS5)
- No proxy: hyper raw write direct connection

Header handling improvements:
- Replace host header in-place at original position instead of
  skip-and-append, preserving client's header ordering
- Preserve client's original accept-encoding for transparent passthrough;
  only force identity encoding when transform path needs decompression
- Add should_force_identity_encoding() to centralize the decision
- Remove hardcoded 'br, gzip, deflate' override that masked client values

Proxy URL resolution (priority order):
1. Provider-specific proxy config (if enabled)
2. Global proxy URL configured in CC Switch
3. Direct connection (no proxy)
This commit is contained in:
YoVinchen
2026-03-28 12:17:42 +08:00
parent 3e4c87278f
commit 905f7ccbfe
+141 -17
View File
@@ -794,6 +794,12 @@ impl RequestForwarder {
// 过滤私有参数(以 `_` 开头的字段),防止内部信息泄露到上游
// 默认使用空白名单,过滤所有 _ 前缀字段
let filtered_body = filter_private_params_with_whitelist(request_body, &[]);
let force_identity_encoding = should_force_identity_encoding(
effective_endpoint,
&filtered_body,
headers,
needs_transform,
);
// 获取认证头(提前准备,用于内联替换)
let auth_headers = if let Some(mut auth) = adapter.extract_auth(provider) {
@@ -866,6 +872,12 @@ impl RequestForwarder {
&[]
};
// 预计算上游 host 值(用于在原位替换 host header
let upstream_host = url
.parse::<http::Uri>()
.ok()
.and_then(|u| u.authority().map(|a| a.to_string()));
// 预计算 anthropic-beta 值(仅 Claude
let anthropic_beta_value = if adapter.name() == "Claude" {
const CLAUDE_CODE_BETA: &str = "claude-code-20250219";
@@ -898,11 +910,20 @@ impl RequestForwarder {
for (key, value) in headers {
let key_str = key.as_str();
// --- host — 原位替换为上游 host(保持客户端原始位置) ---
if key_str.eq_ignore_ascii_case("host") {
if let Some(ref host_val) = upstream_host {
if let Ok(hv) = http::HeaderValue::from_str(host_val) {
ordered_headers.append(key.clone(), hv);
}
}
continue;
}
// --- 连接 / 追踪 / CDN 类 — 无条件跳过 ---
if matches!(
key_str,
"host"
| "content-length"
"content-length"
| "transfer-encoding"
| "x-forwarded-host"
| "x-forwarded-port"
@@ -947,14 +968,18 @@ impl RequestForwarder {
continue;
}
// --- accept-encoding — 替换为与直连 HTTPS 一致的值 ---
// --- accept-encoding — transform / SSE 路径强制 identity,其余保留原值 ---
if key_str.eq_ignore_ascii_case("accept-encoding") {
if !saw_accept_encoding {
saw_accept_encoding = true;
ordered_headers.append(
http::header::ACCEPT_ENCODING,
http::HeaderValue::from_static("br, gzip, deflate"),
);
if force_identity_encoding {
ordered_headers.append(
http::header::ACCEPT_ENCODING,
http::HeaderValue::from_static("identity"),
);
} else {
ordered_headers.append(key.clone(), value.clone());
}
}
continue;
}
@@ -998,11 +1023,11 @@ impl RequestForwarder {
}
}
// 如果原始请求中没有 accept-encoding,追加
if !saw_accept_encoding {
// transform / SSE 路径在缺失时补 identity;普通透传不主动补 accept-encoding
if !saw_accept_encoding && force_identity_encoding {
ordered_headers.append(
http::header::ACCEPT_ENCODING,
http::HeaderValue::from_static("br, gzip, deflate"),
http::HeaderValue::from_static("identity"),
);
}
@@ -1057,18 +1082,27 @@ impl RequestForwarder {
self.non_streaming_timeout
};
// 检查是否需要通过代理发送(供应商单独代理全局代理)
// 解析上游代理 URL(供应商单独代理 > 全局代理 > 无
let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref());
let use_reqwest_proxy = proxy_config.map(|c| c.enabled).unwrap_or(false)
|| super::http_client::get_current_proxy_url().is_some();
let upstream_proxy_url: Option<String> = proxy_config
.filter(|c| c.enabled)
.and_then(super::http_client::build_proxy_url_from_config)
.or_else(super::http_client::get_current_proxy_url);
// SOCKS5 代理不支持 CONNECT 隧道,需要用 reqwest
let is_socks_proxy = upstream_proxy_url
.as_deref()
.map(|u| u.starts_with("socks5"))
.unwrap_or(false);
let uri: http::Uri = url
.parse()
.map_err(|e| ProxyError::ForwardFailed(format!("Invalid URL '{url}': {e}")))?;
// 发送请求
let response = if use_reqwest_proxy {
// 回退到 reqwest(支持 HTTP/SOCKS5 代理隧道
let response = if is_socks_proxy {
// SOCKS5 代理:只能走 reqwest支持 header case 保留
log::debug!("[Forwarder] Using reqwest for SOCKS5 proxy");
let client = super::http_client::get_for_provider(proxy_config);
let mut request = client.post(&url);
if !self.non_streaming_timeout.is_zero() {
@@ -1088,14 +1122,23 @@ impl RequestForwarder {
})?;
ProxyResponse::Reqwest(reqwest_resp)
} else {
// 主路径:使用 hyper client(保持 header case + order
// HTTP 代理或直连:走 hyper raw write(保持 header 大小写
// 如果有 HTTP 代理,hyper_client 会用 CONNECT 隧道穿过代理
let http_proxy = if is_socks_proxy {
None
} else {
upstream_proxy_url.as_deref()
};
let mut ext = extensions.clone();
ext.insert(super::hyper_client::ExtensionDebugMarker);
super::hyper_client::send_request(
uri,
http::Method::POST,
ordered_headers,
extensions.clone(),
ext,
body_bytes,
timeout,
http_proxy,
)
.await?
};
@@ -1262,6 +1305,22 @@ fn extract_json_error_message(body: &Value) -> Option<String> {
.find_map(|value| value.as_str().map(ToString::to_string))
}
/// Determine whether to force `accept-encoding: identity` on the upstream request.
///
/// Only forced for **transform** paths where the proxy must decompress and re-encode
/// the response body. For transparent passthrough (including streaming / SSE), the
/// client's original `accept-encoding` is preserved so the wire behaviour matches a
/// direct connection. SSE usage parsing may fail on compressed streams, but the bytes
/// are still forwarded correctly to the client.
fn should_force_identity_encoding(
_endpoint: &str,
_body: &Value,
_headers: &axum::http::HeaderMap,
needs_transform: bool,
) -> bool {
needs_transform
}
fn summarize_text_for_log(text: &str, max_chars: usize) -> String {
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
let trimmed = normalized.trim();
@@ -1278,6 +1337,7 @@ fn summarize_text_for_log(text: &str, max_chars: usize) -> String {
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{header::ACCEPT, HeaderMap, HeaderValue};
use serde_json::json;
#[test]
@@ -1344,4 +1404,68 @@ mod tests {
assert_eq!(summary, "line1 line2...");
}
#[test]
fn force_identity_for_transform_requests() {
let headers = HeaderMap::new();
assert!(should_force_identity_encoding(
"/v1/messages",
&json!({ "model": "gpt-5" }),
&headers,
true,
));
}
#[test]
fn transparent_encoding_for_streaming_passthrough() {
let headers = HeaderMap::new();
// Streaming passthrough should NOT force identity — transparent proxy
assert!(!should_force_identity_encoding(
"/v1/responses",
&json!({ "stream": true }),
&headers,
false,
));
}
#[test]
fn transparent_encoding_for_gemini_stream_endpoints() {
let headers = HeaderMap::new();
// SSE endpoints without transform: transparent
assert!(!should_force_identity_encoding(
"/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse",
&json!({ "model": "gemini-2.5-pro" }),
&headers,
false,
));
}
#[test]
fn transparent_encoding_for_sse_accept_header() {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream"));
// SSE accept header without transform: transparent
assert!(!should_force_identity_encoding(
"/v1/responses",
&json!({ "model": "gpt-5" }),
&headers,
false,
));
}
#[test]
fn non_streaming_requests_keep_original_encoding_behavior() {
let headers = HeaderMap::new();
assert!(!should_force_identity_encoding(
"/v1/responses",
&json!({ "model": "gpt-5" }),
&headers,
false,
));
}
}