fix(api): stop CORS assembly from panicking on configured origins

Replace the wildcard allow_headers with an explicit whitelist (authorization, content-type, x-api-key, and the configured request-id header): tower-http asserts against wildcard headers combined with allow_credentials, so any non-empty cors_allowed_origins previously panicked inside initialize() before the server could listen. Add a preflight assembly test covering a configured origin.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
zyycn
2026-07-26 19:57:13 +08:00
co-authored by Claude
parent 2e0bee6096
commit 6deecdebe7
4 changed files with 69 additions and 6 deletions
+8 -2
View File
@@ -16,7 +16,7 @@ use gateway_core::engine::execution::ExecutionService;
use gateway_core::health::{HealthProbe, WorkerHealthSource};
use gateway_core::lifecycle::ConnectionLifecycle;
use serde::Deserialize;
use tower_http::cors::{Any, CorsLayer};
use tower_http::cors::CorsLayer;
use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
use tower_http::services::{ServeDir, ServeFile};
use tower_http::timeout::TimeoutLayer;
@@ -146,11 +146,17 @@ pub fn initialize(
.map_err(|_| ApiError::Config(ApiConfigError::InvalidCorsOrigin))
})
.collect::<Result<Vec<_>, _>>()?;
// credentials 模式禁止通配 allow_headers;显式列出鉴权与内容协商头。
router = router.layer(
CorsLayer::new()
.allow_origin(origins)
.allow_methods([Method::GET, Method::POST])
.allow_headers(Any)
.allow_headers([
axum::http::header::AUTHORIZATION,
axum::http::header::CONTENT_TYPE,
HeaderName::from_static("x-api-key"),
request_id_header.clone(),
])
.allow_credentials(true),
);
}
@@ -39,11 +39,18 @@ use gateway_core::routing::{
};
pub(super) async fn api_router(execution: Arc<dyn ExecutionService>) -> axum::Router {
api_router_with_origins(execution, Vec::new()).await
}
pub(super) async fn api_router_with_origins(
execution: Arc<dyn ExecutionService>,
cors_allowed_origins: Vec<String>,
) -> axum::Router {
let admin = crate::admin::AdminTestFixture::new().await;
gateway_api::initialize(
gateway_api::ApiConfig {
asset_directory: std::env::temp_dir(),
cors_allowed_origins: Vec::new(),
cors_allowed_origins,
request_timeout_seconds: None,
request_id_header: "x-request-id".to_owned(),
},
@@ -15,13 +15,13 @@ use tower::ServiceExt;
use super::{api_router, authenticated_client};
struct ModelsExecution {
pub(super) struct ModelsExecution {
client: AuthenticatedClient,
profiles: bool,
}
impl ModelsExecution {
fn new() -> Arc<Self> {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self {
client: authenticated_client("sk_models_test"),
profiles: false,
@@ -2,12 +2,15 @@ use axum::{
Router,
body::{Body, Bytes},
extract::DefaultBodyLimit,
http::{Request, StatusCode},
http::{Method, Request, StatusCode},
routing::post,
};
use gateway_api::openai::router::MAX_CLIENT_REQUEST_BODY_BYTES;
use tower::ServiceExt;
use super::api_router_with_origins;
use super::models::ModelsExecution;
fn body_limit_app() -> Router {
Router::new()
.route(
@@ -50,3 +53,50 @@ async fn responses_body_limit_should_reject_one_byte_over_sixteen_mibibytes() {
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn configured_cors_origin_assembles_router_and_answers_preflight() {
let router = api_router_with_origins(
ModelsExecution::new(),
vec!["https://app.example.com".into()],
)
.await;
let response = router
.oneshot(
Request::builder()
.method(Method::OPTIONS)
.uri("/v1/models")
.header("origin", "https://app.example.com")
.header("access-control-request-method", "GET")
.header("access-control-request-headers", "authorization")
.body(Body::empty())
.expect("build preflight request"),
)
.await
.expect("route preflight request");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("access-control-allow-origin")
.and_then(|value| value.to_str().ok()),
Some("https://app.example.com")
);
assert_eq!(
response
.headers()
.get("access-control-allow-credentials")
.and_then(|value| value.to_str().ok()),
Some("true")
);
let allow_headers = response
.headers()
.get("access-control-allow-headers")
.and_then(|value| value.to_str().ok())
.expect("allow-headers present");
assert!(allow_headers.contains("authorization"));
assert!(allow_headers.contains("x-api-key"));
assert!(allow_headers.contains("x-request-id"));
}