Invalid CORS configuration: Cannot combine Access-Control-Allow-Credentials: true with Access-Control-Allow-Headers: *
主线位置:不在请求路上,在装配时。
CorsLayer被layer()的那一刻检查配置,非法就 panic;直接用Cors::new(..)拼出来的配置推迟到第一次poll_ready。两处检查和谓词凭据的缺口在 CORS 的内部 里讲过,这一页只看最常见的那种写法。
flowchart TB
cfg["CorsLayer::permissive():四个 Any"] --> cred[".allow_credentials(true)"]
cred --> layer["Router::layer 调 Layer::layer"]
layer --> check{"ensure_usable_cors_rules"}
check -->|"Allow-Headers 是 *"| p1["panic:Allow-Headers"]
check -->|"改成 mirror_request"| ok["构造成功"]
复现
开发时图省事写 permissive(),接上前端登录后加一句 allow_credentials(true) 让浏览器带 cookie:
tower-http = { version = "=0.7.1", features = ["cors"] }
axum = { version = "=0.8.9", default-features = false }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::http::{header, Method, Request};
use axum::routing::get;
use axum::Router;
use std::panic;
use tower::ServiceExt;
use tower_http::cors::CorsLayer;
fn app(cors: CorsLayer) -> Router {
Router::new().route("/", get(|| async { "hi" })).layer(cors)
}
fn try_build(name: &str, f: impl FnOnce() -> Router + panic::UnwindSafe) {
match panic::catch_unwind(f) {
Ok(_) => println!("{name}: built"),
Err(e) => {
let msg = e.downcast_ref::<String>().cloned().or(e.downcast_ref::<&str>().map(|s| s.to_string())).unwrap();
println!("{name}: panicked: {msg}");
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// 打印 panic 的位置,只留 crate 内的相对路径
panic::set_hook(Box::new(|info| {
let loc = info.location().unwrap();
let file = loc.file().rsplit_once("src/").map(|(_, f)| f).unwrap_or(loc.file());
println!(" (panic at tower-http src/{file}:{})", loc.line());
}));
try_build("permissive().allow_credentials(true)", || app(CorsLayer::permissive().allow_credentials(true)));
try_build("very_permissive()", || app(CorsLayer::very_permissive()));
try_build("headers listed, rest Any", || app(CorsLayer::permissive().allow_headers([header::CONTENT_TYPE]).allow_credentials(true)));
try_build("route added after layer", || {
Router::new().layer(CorsLayer::permissive().allow_credentials(true)).route("/", get(|| async { "hi" }))
});
// very_permissive 的预检响应
let req = Request::builder()
.method(Method::OPTIONS)
.uri("/")
.header(header::ORIGIN, "https://a.example")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "PUT")
.header(header::ACCESS_CONTROL_REQUEST_HEADERS, "x-token")
.body(Body::empty())
.unwrap();
let res = app(CorsLayer::very_permissive()).oneshot(req).await.unwrap();
println!("very_permissive preflight: {}", res.status());
for (k, v) in res.headers() {
if k.as_str().starts_with("access-control") || k == header::VARY {
println!(" {k}: {}", v.to_str().unwrap());
}
}
}
M2 Max、rustc 1.99.0,跑 3 次输出相同:
(panic at tower-http src/cors/mod.rs:822)
permissive().allow_credentials(true): panicked: Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` with `Access-Control-Allow-Headers: *`
very_permissive(): built
(panic at tower-http src/cors/mod.rs:828)
headers listed, rest Any: panicked: Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` with `Access-Control-Allow-Methods: *`
(panic at tower-http src/cors/mod.rs:822)
route added after layer: panicked: Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` with `Access-Control-Allow-Headers: *`
very_permissive preflight: 200 OK
access-control-allow-credentials: true
vary: origin, access-control-request-method, access-control-request-headers
access-control-allow-methods: PUT
access-control-allow-headers: x-token
access-control-allow-origin: https://a.example
几点:
- 报的是
Allow-Headers,不是Allow-Origin。多数人以为问题在*来源上,报错却指向请求头。ensure_usable_cors_rules依次断言Allow-Headers、Allow-Methods、Allow-Origin、Expose-Headers都不是*,permissive()把四个全设成了Any,第一条就失败。headers listed那一行把请求头列出来,报错立刻换成下一条Allow-Methods(第 828 行),四个都改完才过。 - panic 发生在装配时。axum 的
Router::layer在构造路由表时就对每个端点调Layer::layer,没有路由时也会套在 fallback 上,所以route added after layer(先.layer后.route)同样 panic。服务起不来,比请求时才出错好查。 permissive()本身合法,问题只在和凭据组合。浏览器规定带凭据的 CORS 响应里这四个头不能是*,tower-http 把浏览器会拒绝的配置提前变成 panic。
修法
要带凭据又不想逐个列,用 very_permissive():凭据打开,来源、方法、请求头都用 mirror_request,把预检请求里的值原样写回。上面预检的输出就是这样:Origin: https://a.example 回成 access-control-allow-origin: https://a.example,PUT、x-token 同理,Vary 带上三个请求头。
它等于对任何来源都放行带凭据的请求,和 * 加凭据在效果上一样宽,只是写成了浏览器接受的形式,适合本地开发。线上应当列出来源:
CorsLayer::new()
.allow_credentials(true)
.allow_origin([HeaderValue::from_static("https://app.example")])
.allow_methods([Method::GET, Method::POST])
.allow_headers([header::CONTENT_TYPE])
配置来自配置文件、不想让坏配置把进程打崩时,0.7.1 只能自己在构造前检查。tag 之后合入的 validate()(#747,未发版)返回错误而不是 panic,见 CORS 的内部 最后一节。
小结
- 凭据加任何一个
*都会 panic,permissive()加凭据先撞上Allow-Headers那一条。 - panic 在
Router::layer时发生,和路由的先后无关。 - 开发用
very_permissive(),线上把来源、方法、头列出来;谓词形式的凭据能绕过检查,那是另一个坑,见 CORS 的内部。
💬 关联讨论
发起讨论加载中...