Rust in Everything
基于 tower-http 0.7.1

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 的内部。

💬 关联讨论

发起讨论
加载中...