fix(proxy): return Result from get_auth_headers to avoid panic on bad credentials

User-pasted API keys can contain control chars or CR/LF that make
HeaderValue::from_str return Err; the previous unwrap inside every
adapter turned such input into a process-wide panic instead of a request
error. The trait now returns Result<_, ProxyError>; Claude/Codex/Gemini
impls propagate ProxyError::AuthError so the client sees a 401 with the
underlying parse error instead of a crash. Adds a regression test that
pastes a CRLF-containing key and asserts AuthError.
This commit is contained in:
Jason
2026-05-13 23:12:00 +08:00
parent 58648a9c53
commit c9a6afc0b7
5 changed files with 65 additions and 43 deletions
+1 -1
View File
@@ -1151,7 +1151,7 @@ impl RequestForwarder {
}
}
adapter.get_auth_headers(&auth)
adapter.get_auth_headers(&auth)?
} else {
Vec::new()
};
+8 -1
View File
@@ -30,7 +30,14 @@ pub trait ProviderAdapter: Send + Sync {
///
/// The forwarder inserts these at the position of the original auth header
/// so that header order is preserved.
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)>;
///
/// Returns `ProxyError::AuthError` when the credential contains characters
/// that cannot be encoded as an HTTP header value (e.g. control chars,
/// CR/LF), which would otherwise panic inside `HeaderValue::from_str`.
fn get_auth_headers(
&self,
auth: &AuthInfo,
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError>;
/// 是否需要格式转换
fn needs_transform(&self, _provider: &Provider) -> bool {
+35 -32
View File
@@ -589,33 +589,34 @@ impl ProviderAdapter for ClaudeAdapter {
base
}
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)> {
fn get_auth_headers(
&self,
auth: &AuthInfo,
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {
use http::{HeaderName, HeaderValue};
// 注意:anthropic-version 由 forwarder.rs 统一处理(透传客户端值或设置默认值)
let bearer = format!("Bearer {}", auth.api_key);
match auth.strategy {
let hv = |s: &str| -> Result<HeaderValue, ProxyError> {
HeaderValue::from_str(s)
.map_err(|e| ProxyError::AuthError(format!("invalid auth header value: {e}")))
};
Ok(match auth.strategy {
AuthStrategy::Anthropic => {
vec![(
HeaderName::from_static("x-api-key"),
HeaderValue::from_str(&auth.api_key).unwrap(),
)]
vec![(HeaderName::from_static("x-api-key"), hv(&auth.api_key)?)]
}
AuthStrategy::ClaudeAuth | AuthStrategy::Bearer => {
vec![(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&bearer).unwrap(),
)]
vec![(HeaderName::from_static("authorization"), hv(&bearer)?)]
}
AuthStrategy::Google => vec![(
HeaderName::from_static("x-goog-api-key"),
HeaderValue::from_str(&auth.api_key).unwrap(),
hv(&auth.api_key)?,
)],
AuthStrategy::GoogleOAuth => {
let token = auth.access_token.as_ref().unwrap_or(&auth.api_key);
vec![
(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
hv(&format!("Bearer {token}"))?,
),
(
HeaderName::from_static("x-goog-api-client"),
@@ -627,10 +628,7 @@ impl ProviderAdapter for ClaudeAdapter {
// 注意:bearer token 由 forwarder 动态注入到 auth.api_key
// ChatGPT-Account-Id 由 forwarder 注入额外 header
vec![
(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&bearer).unwrap(),
),
(HeaderName::from_static("authorization"), hv(&bearer)?),
(
HeaderName::from_static("originator"),
HeaderValue::from_static("cc-switch"),
@@ -641,10 +639,7 @@ impl ProviderAdapter for ClaudeAdapter {
// 生成请求追踪 ID
let request_id = uuid::Uuid::new_v4().to_string();
vec![
(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&bearer).unwrap(),
),
(HeaderName::from_static("authorization"), hv(&bearer)?),
(
HeaderName::from_static("editor-version"),
HeaderValue::from_static(super::copilot_auth::COPILOT_EDITOR_VERSION),
@@ -683,17 +678,11 @@ impl ProviderAdapter for ClaudeAdapter {
HeaderName::from_static("x-vscode-user-agent-library-version"),
HeaderValue::from_static("electron-fetch"),
),
(
HeaderName::from_static("x-request-id"),
HeaderValue::from_str(&request_id).unwrap(),
),
(
HeaderName::from_static("x-agent-task-id"),
HeaderValue::from_str(&request_id).unwrap(),
),
(HeaderName::from_static("x-request-id"), hv(&request_id)?),
(HeaderName::from_static("x-agent-task-id"), hv(&request_id)?),
]
}
}
})
}
fn needs_transform(&self, provider: &Provider) -> bool {
@@ -871,7 +860,7 @@ mod tests {
let adapter = ClaudeAdapter::new();
let auth = AuthInfo::new("sk-ant-test".to_string(), AuthStrategy::Anthropic);
let headers = adapter.get_auth_headers(&auth);
let headers = adapter.get_auth_headers(&auth).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0.as_str(), "x-api-key");
assert_eq!(headers[0].1.to_str().unwrap(), "sk-ant-test");
@@ -882,7 +871,7 @@ mod tests {
let adapter = ClaudeAdapter::new();
let auth = AuthInfo::new("sk-relay-test".to_string(), AuthStrategy::ClaudeAuth);
let headers = adapter.get_auth_headers(&auth);
let headers = adapter.get_auth_headers(&auth).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0.as_str(), "authorization");
assert_eq!(headers[0].1.to_str().unwrap(), "Bearer sk-relay-test");
@@ -893,12 +882,26 @@ mod tests {
let adapter = ClaudeAdapter::new();
let auth = AuthInfo::new("sk-or-test".to_string(), AuthStrategy::Bearer);
let headers = adapter.get_auth_headers(&auth);
let headers = adapter.get_auth_headers(&auth).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0.as_str(), "authorization");
assert_eq!(headers[0].1.to_str().unwrap(), "Bearer sk-or-test");
}
#[test]
fn test_get_auth_headers_rejects_illegal_header_chars() {
// 用户粘贴含 \r\n 的"脏"key 不能让进程 panic
let adapter = ClaudeAdapter::new();
let auth = AuthInfo::new(
"sk-ant-bad\r\nX-Inject: 1".to_string(),
AuthStrategy::Anthropic,
);
let result = adapter.get_auth_headers(&auth);
assert!(result.is_err(), "expected AuthError, got Ok");
assert!(matches!(result, Err(ProxyError::AuthError(_))));
}
#[test]
fn test_extract_auth_openrouter() {
let adapter = ClaudeAdapter::new();
+9 -4
View File
@@ -173,12 +173,17 @@ impl ProviderAdapter for CodexAdapter {
url
}
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)> {
fn get_auth_headers(
&self,
auth: &AuthInfo,
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {
let bearer = format!("Bearer {}", auth.api_key);
vec![(
let value = http::HeaderValue::from_str(&bearer)
.map_err(|e| ProxyError::AuthError(format!("invalid auth header value: {e}")))?;
Ok(vec![(
http::HeaderName::from_static("authorization"),
http::HeaderValue::from_str(&bearer).unwrap(),
)]
value,
)])
}
}
+12 -5
View File
@@ -228,15 +228,22 @@ impl ProviderAdapter for GeminiAdapter {
url
}
fn get_auth_headers(&self, auth: &AuthInfo) -> Vec<(http::HeaderName, http::HeaderValue)> {
fn get_auth_headers(
&self,
auth: &AuthInfo,
) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {
use http::{HeaderName, HeaderValue};
match auth.strategy {
let hv = |s: &str| -> Result<HeaderValue, ProxyError> {
HeaderValue::from_str(s)
.map_err(|e| ProxyError::AuthError(format!("invalid auth header value: {e}")))
};
Ok(match auth.strategy {
AuthStrategy::GoogleOAuth => {
let token = auth.access_token.as_ref().unwrap_or(&auth.api_key);
vec![
(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
hv(&format!("Bearer {token}"))?,
),
(
HeaderName::from_static("x-goog-api-client"),
@@ -246,9 +253,9 @@ impl ProviderAdapter for GeminiAdapter {
}
_ => vec![(
HeaderName::from_static("x-goog-api-key"),
HeaderValue::from_str(&auth.api_key).unwrap(),
hv(&auth.api_key)?,
)],
}
})
}
}