mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(proxy): forward client HTTP method instead of hard-coding POST
The forwarder used to call client.post(&url) / http::Method::POST in both the reqwest and hyper paths, and the Gemini route table only registered POST /v1beta/*. As a result anything the Gemini SDK / CLI sent as GET (models list, models/<id> info) hit a 404 at the router and bypassed the local proxy's stats, rectifiers, and failover. Thread the request method end-to-end: - ProviderAdapter forwarder API now takes the http::Method by reference per attempt and dispatches client.request(method, &url) for reqwest and method.clone() for the hyper raw path. - All five callers in handlers.rs (handle_messages_for_app for Claude / Claude Desktop, handle_chat_completions, handle_responses, handle_responses_compact, handle_gemini) pull the method out of the incoming axum::extract::Request and pass it on. - handle_gemini tolerates an empty body (GET endpoints have none) and the forwarder skips serializing / sending a body for GET / HEAD — attaching JSON to a GET makes Gemini reject the request. - server.rs swaps the Gemini routes to any(handle_gemini) so the same handler handles GET / POST / PUT / DELETE, and adds /gemini/v1/* for the GA path version.
This commit is contained in:
@@ -160,9 +160,11 @@ impl RequestForwarder {
|
||||
/// `active_connections` / 刷新 `last_request_at`,无论 inner 走哪条出口路径,
|
||||
/// 出口处都会把 `active_connections` 回收。Per-attempt 维度(成功/失败/熔断
|
||||
/// 等)仍由 inner 内自行更新 `success_requests` / `failed_requests`。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn forward_with_retry(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
method: http::Method,
|
||||
endpoint: &str,
|
||||
body: Value,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -176,7 +178,9 @@ impl RequestForwarder {
|
||||
s.last_request_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
}
|
||||
let result = self
|
||||
.forward_with_retry_inner(app_type, endpoint, body, headers, extensions, providers)
|
||||
.forward_with_retry_inner(
|
||||
app_type, method, endpoint, body, headers, extensions, providers,
|
||||
)
|
||||
.await;
|
||||
{
|
||||
let mut s = self.status.write().await;
|
||||
@@ -189,13 +193,16 @@ impl RequestForwarder {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `app_type` - 应用类型
|
||||
/// * `method` - 客户端请求的 HTTP 方法(透传给上游,支持 GET/POST 等)
|
||||
/// * `endpoint` - API 端点
|
||||
/// * `body` - 请求体
|
||||
/// * `headers` - 请求头
|
||||
/// * `providers` - 已选择的 Provider 列表(由 RequestContext 提供,避免重复调用 select_providers)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn forward_with_retry_inner(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
method: http::Method,
|
||||
endpoint: &str,
|
||||
body: Value,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -286,6 +293,7 @@ impl RequestForwarder {
|
||||
match self
|
||||
.forward(
|
||||
app_type,
|
||||
&method,
|
||||
provider,
|
||||
endpoint,
|
||||
&provider_body,
|
||||
@@ -410,6 +418,7 @@ impl RequestForwarder {
|
||||
match self
|
||||
.forward(
|
||||
app_type,
|
||||
&method,
|
||||
provider,
|
||||
endpoint,
|
||||
&provider_body,
|
||||
@@ -616,6 +625,7 @@ impl RequestForwarder {
|
||||
match self
|
||||
.forward(
|
||||
app_type,
|
||||
&method,
|
||||
provider,
|
||||
endpoint,
|
||||
&provider_body,
|
||||
@@ -864,6 +874,7 @@ impl RequestForwarder {
|
||||
async fn forward(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
method: &http::Method,
|
||||
provider: &Provider,
|
||||
endpoint: &str,
|
||||
body: &Value,
|
||||
@@ -1493,9 +1504,15 @@ impl RequestForwarder {
|
||||
ordered_headers.insert(name, value);
|
||||
}
|
||||
|
||||
// 序列化请求体
|
||||
let body_bytes = serde_json::to_vec(&filtered_body)
|
||||
.map_err(|e| ProxyError::Internal(format!("Failed to serialize request body: {e}")))?;
|
||||
// 序列化请求体。GET/HEAD 是 idempotent/safe 方法,按 HTTP 语义不应携带 body;
|
||||
// 强行附带 JSON body 会让某些上游(如 Google Gemini 的 models.list)拒绝请求。
|
||||
let body_bytes = if matches!(method, &http::Method::GET | &http::Method::HEAD) {
|
||||
Vec::new()
|
||||
} else {
|
||||
serde_json::to_vec(&filtered_body).map_err(|e| {
|
||||
ProxyError::Internal(format!("Failed to serialize request body: {e}"))
|
||||
})?
|
||||
};
|
||||
|
||||
// 确保 content-type 存在
|
||||
if !ordered_headers.contains_key(http::header::CONTENT_TYPE) {
|
||||
@@ -1553,7 +1570,7 @@ impl RequestForwarder {
|
||||
"[Forwarder] Using pooled reqwest client (preserve_exact_header_case={preserve_exact_header_case}, socks_proxy={is_socks_proxy})"
|
||||
);
|
||||
let client = super::http_client::get();
|
||||
let mut request = client.post(&url);
|
||||
let mut request = client.request(method.clone(), &url);
|
||||
let request_is_streaming =
|
||||
is_streaming_request(&effective_endpoint, &filtered_body, headers);
|
||||
if request_is_streaming {
|
||||
@@ -1594,7 +1611,7 @@ impl RequestForwarder {
|
||||
.map_err(|e| ProxyError::ForwardFailed(format!("Invalid URL '{url}': {e}")))?;
|
||||
super::hyper_client::send_request(
|
||||
uri,
|
||||
http::Method::POST,
|
||||
method.clone(),
|
||||
ordered_headers,
|
||||
extensions.clone(),
|
||||
body_bytes,
|
||||
|
||||
@@ -115,6 +115,7 @@ async fn handle_messages_for_app(
|
||||
strip_prefix: Option<&'static str>,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
let (parts, body) = request.into_parts();
|
||||
let method = parts.method.clone();
|
||||
let uri = parts.uri;
|
||||
let headers = parts.headers;
|
||||
let extensions = parts.extensions;
|
||||
@@ -147,6 +148,7 @@ async fn handle_messages_for_app(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&app_type,
|
||||
method,
|
||||
endpoint,
|
||||
body.clone(),
|
||||
headers,
|
||||
@@ -453,6 +455,7 @@ pub async fn handle_chat_completions(
|
||||
request: axum::extract::Request,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
let (parts, req_body) = request.into_parts();
|
||||
let method = parts.method.clone();
|
||||
let uri = parts.uri;
|
||||
let headers = parts.headers;
|
||||
let extensions = parts.extensions;
|
||||
@@ -477,6 +480,7 @@ pub async fn handle_chat_completions(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Codex,
|
||||
method,
|
||||
&endpoint,
|
||||
body,
|
||||
headers,
|
||||
@@ -507,6 +511,7 @@ pub async fn handle_responses(
|
||||
request: axum::extract::Request,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
let (parts, req_body) = request.into_parts();
|
||||
let method = parts.method.clone();
|
||||
let uri = parts.uri;
|
||||
let headers = parts.headers;
|
||||
let extensions = parts.extensions;
|
||||
@@ -531,6 +536,7 @@ pub async fn handle_responses(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Codex,
|
||||
method,
|
||||
&endpoint,
|
||||
body,
|
||||
headers,
|
||||
@@ -561,6 +567,7 @@ pub async fn handle_responses_compact(
|
||||
request: axum::extract::Request,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
let (parts, req_body) = request.into_parts();
|
||||
let method = parts.method.clone();
|
||||
let uri = parts.uri;
|
||||
let headers = parts.headers;
|
||||
let extensions = parts.extensions;
|
||||
@@ -585,6 +592,7 @@ pub async fn handle_responses_compact(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Codex,
|
||||
method,
|
||||
&endpoint,
|
||||
body,
|
||||
headers,
|
||||
@@ -620,6 +628,7 @@ pub async fn handle_gemini(
|
||||
request: axum::extract::Request,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
let (parts, req_body) = request.into_parts();
|
||||
let method = parts.method.clone();
|
||||
let headers = parts.headers;
|
||||
let extensions = parts.extensions;
|
||||
let body_bytes = req_body
|
||||
@@ -627,8 +636,14 @@ pub async fn handle_gemini(
|
||||
.await
|
||||
.map_err(|e| ProxyError::Internal(format!("Failed to read request body: {e}")))?
|
||||
.to_bytes();
|
||||
let body: Value = serde_json::from_slice(&body_bytes)
|
||||
.map_err(|e| ProxyError::Internal(format!("Failed to parse request body: {e}")))?;
|
||||
// GET 类只读端点(/v1beta/models、/v1beta/models/<model> 等)没有请求体,
|
||||
// 不能强制 parse 为 JSON —— 否则空 body 会被拒绝。
|
||||
let body: Value = if body_bytes.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
serde_json::from_slice(&body_bytes)
|
||||
.map_err(|e| ProxyError::Internal(format!("Failed to parse request body: {e}")))?
|
||||
};
|
||||
|
||||
// Gemini 的模型名称在 URI 中
|
||||
let mut ctx = RequestContext::new(&state, &body, &headers, AppType::Gemini, "Gemini", "gemini")
|
||||
@@ -650,6 +665,7 @@ pub async fn handle_gemini(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Gemini,
|
||||
method,
|
||||
endpoint,
|
||||
body,
|
||||
headers,
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::{
|
||||
use crate::database::Database;
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{get, post},
|
||||
routing::{any, get, post},
|
||||
Router,
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
@@ -331,8 +331,15 @@ impl ProxyServer {
|
||||
post(handlers::handle_responses_compact),
|
||||
)
|
||||
// Gemini API (支持带前缀和不带前缀)
|
||||
.route("/v1beta/*path", post(handlers::handle_gemini))
|
||||
.route("/gemini/v1beta/*path", post(handlers::handle_gemini))
|
||||
//
|
||||
// 用 `any(..)` 覆盖所有 HTTP 方法:除了 POST `:generateContent` /
|
||||
// `:streamGenerateContent` / `:countTokens` 之外,Gemini SDK / CLI 还会发
|
||||
// GET `/models`、GET `/models/<id>` 等只读端点。如果只挂 POST,这些 GET
|
||||
// 请求会在路由层 404,绕过本地代理的统计、整流和故障转移。
|
||||
.route("/v1beta/*path", any(handlers::handle_gemini))
|
||||
.route("/gemini/v1beta/*path", any(handlers::handle_gemini))
|
||||
// Gemini 的 GA 版本也叫 /v1,给原 SDK 留一条出口
|
||||
.route("/gemini/v1/*path", any(handlers::handle_gemini))
|
||||
// 提高默认请求体大小限制(避免 413 Payload Too Large)
|
||||
.layer(DefaultBodyLimit::max(200 * 1024 * 1024))
|
||||
.with_state(self.state.clone())
|
||||
|
||||
Reference in New Issue
Block a user