mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
f4e2c28a2b
When forward_with_retry fails for Codex endpoints (/chat/completions, /responses, /responses/compact), the handlers previously returned the bare ProxyError, surfacing a terse body to the client. Build a richer JSON error instead: the message embeds provider, model, endpoint and upstream status, and the error object carries those as structured fields alongside a stable cc_switch_* code. Align map_proxy_error_to_status with ProxyError::into_response so the manually built responses use the canonical status codes (AuthError->401, Config/InvalidRequest->400, TransformError->422, StreamIdleTimeout->504, AlreadyRunning->409, NotRunning->503); previously these forwarder-level errors collapsed to 500.
156 lines
5.3 KiB
Rust
156 lines
5.3 KiB
Rust
//! 错误类型到 HTTP 状态码的映射
|
||
//!
|
||
//! 将 ProxyError 映射到合适的 HTTP 状态码,用于日志记录和手动构建错误响应
|
||
|
||
use super::ProxyError;
|
||
|
||
/// 将 ProxyError 映射到 HTTP 状态码
|
||
///
|
||
/// 映射规则:
|
||
/// - 上游错误:直接使用上游返回的状态码
|
||
/// - 超时:504 Gateway Timeout
|
||
/// - 连接失败:502 Bad Gateway
|
||
/// - 无可用 Provider:503 Service Unavailable
|
||
/// - 重试耗尽:503 Service Unavailable
|
||
/// - 认证错误:401 Unauthorized
|
||
/// - 配置/请求错误:400 Bad Request
|
||
/// - 转换错误:422 Unprocessable Entity
|
||
/// - 其他错误:500 Internal Server Error
|
||
pub fn map_proxy_error_to_status(error: &ProxyError) -> u16 {
|
||
match error {
|
||
// 服务状态错误:与 IntoResponse 保持一致
|
||
ProxyError::AlreadyRunning => 409,
|
||
ProxyError::NotRunning => 503,
|
||
|
||
// 上游错误:使用实际状态码
|
||
ProxyError::UpstreamError { status, .. } => *status,
|
||
|
||
// 超时错误:504 Gateway Timeout
|
||
ProxyError::Timeout(_) | ProxyError::StreamIdleTimeout(_) => 504,
|
||
|
||
// 转发失败/连接失败:502 Bad Gateway
|
||
ProxyError::ForwardFailed(_) => 502,
|
||
|
||
// 无可用 Provider:503 Service Unavailable
|
||
ProxyError::NoAvailableProvider => 503,
|
||
|
||
// 所有供应商已熔断:503 Service Unavailable
|
||
ProxyError::AllProvidersCircuitOpen => 503,
|
||
|
||
// 未配置供应商:503 Service Unavailable
|
||
ProxyError::NoProvidersConfigured => 503,
|
||
|
||
// 重试耗尽:503 Service Unavailable
|
||
ProxyError::MaxRetriesExceeded => 503,
|
||
|
||
// Provider 不健康:503 Service Unavailable
|
||
ProxyError::ProviderUnhealthy(_) => 503,
|
||
|
||
// 配置错误/无效请求:400 Bad Request
|
||
ProxyError::ConfigError(_) | ProxyError::InvalidRequest(_) => 400,
|
||
|
||
// 认证错误:401 Unauthorized
|
||
ProxyError::AuthError(_) => 401,
|
||
|
||
// 数据库错误:500 Internal Server Error
|
||
ProxyError::DatabaseError(_) => 500,
|
||
|
||
// 转换错误:422 Unprocessable Entity
|
||
ProxyError::TransformError(_) => 422,
|
||
|
||
// 其他未知错误:500 Internal Server Error
|
||
_ => 500,
|
||
}
|
||
}
|
||
|
||
/// 将 ProxyError 转换为用户友好的错误消息
|
||
pub fn get_error_message(error: &ProxyError) -> String {
|
||
match error {
|
||
ProxyError::UpstreamError { status, body } => {
|
||
if let Some(body) = body {
|
||
format!("上游错误 ({status}): {body}")
|
||
} else {
|
||
format!("上游错误 ({status})")
|
||
}
|
||
}
|
||
ProxyError::Timeout(msg) => format!("请求超时: {msg}"),
|
||
ProxyError::ForwardFailed(msg) => format!("转发失败: {msg}"),
|
||
ProxyError::NoAvailableProvider => "无可用 Provider".to_string(),
|
||
ProxyError::AllProvidersCircuitOpen => "所有供应商已熔断,无可用渠道".to_string(),
|
||
ProxyError::NoProvidersConfigured => "未配置供应商".to_string(),
|
||
ProxyError::MaxRetriesExceeded => "所有 Provider 都失败,重试耗尽".to_string(),
|
||
ProxyError::ProviderUnhealthy(msg) => format!("Provider 不健康: {msg}"),
|
||
ProxyError::DatabaseError(msg) => format!("数据库错误: {msg}"),
|
||
ProxyError::TransformError(msg) => format!("请求/响应转换错误: {msg}"),
|
||
_ => error.to_string(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_map_upstream_error() {
|
||
let error = ProxyError::UpstreamError {
|
||
status: 401,
|
||
body: Some("Unauthorized".to_string()),
|
||
};
|
||
assert_eq!(map_proxy_error_to_status(&error), 401);
|
||
}
|
||
|
||
#[test]
|
||
fn test_map_timeout_error() {
|
||
let error = ProxyError::Timeout("Request timeout".to_string());
|
||
assert_eq!(map_proxy_error_to_status(&error), 504);
|
||
}
|
||
|
||
#[test]
|
||
fn test_map_connection_error() {
|
||
let error = ProxyError::ForwardFailed("Connection refused".to_string());
|
||
assert_eq!(map_proxy_error_to_status(&error), 502);
|
||
}
|
||
|
||
#[test]
|
||
fn test_map_no_provider_error() {
|
||
let error = ProxyError::NoAvailableProvider;
|
||
assert_eq!(map_proxy_error_to_status(&error), 503);
|
||
}
|
||
|
||
#[test]
|
||
fn test_map_status_matches_proxy_error_response_semantics() {
|
||
assert_eq!(
|
||
map_proxy_error_to_status(&ProxyError::AuthError("bad token".to_string())),
|
||
401
|
||
);
|
||
assert_eq!(
|
||
map_proxy_error_to_status(&ProxyError::ConfigError("bad config".to_string())),
|
||
400
|
||
);
|
||
assert_eq!(
|
||
map_proxy_error_to_status(&ProxyError::InvalidRequest("bad request".to_string())),
|
||
400
|
||
);
|
||
assert_eq!(
|
||
map_proxy_error_to_status(&ProxyError::TransformError("bad transform".to_string())),
|
||
422
|
||
);
|
||
assert_eq!(
|
||
map_proxy_error_to_status(&ProxyError::StreamIdleTimeout(30)),
|
||
504
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_error_message() {
|
||
let error = ProxyError::UpstreamError {
|
||
status: 500,
|
||
body: Some("Internal Server Error".to_string()),
|
||
};
|
||
let msg = get_error_message(&error);
|
||
assert!(msg.contains("上游错误"));
|
||
assert!(msg.contains("500"));
|
||
assert!(msg.contains("Internal Server Error"));
|
||
}
|
||
}
|