Rust in Everything
基于 tower-http 0.7.1

ServeDir 回 500,日志里什么都没有

主线位置:作用点主线的第二个时刻。ServeDir 的 call 返回的 future 在完成时把 IO 错误变成 404 或 500,只有 500 那一支打日志,而那一行日志挂在 #[cfg(feature = "tracing")] 后面。隐式 feature 和 #614 的来历见 源码地图 的「没有蠢问题」;这一页把哪些错误变 500、日志在什么配置下出现跑出来。

flowchart TB
  err["try_call 返回 io::Error"] --> nf{"should_return_not_found"}
  nf -->|"NotFound、PermissionDenied、ENOTDIR"| s404["404,不打日志"]
  nf -->|"其余"| cfg{"编译时开了 tracing feature 吗"}
  cfg -->|是| log["tracing::error!:Failed to read file"]
  cfg -->|否| quiet["什么都不打"]
  log --> s500["500"]
  quiet --> s500

复现

三种请求:没有读权限的文件、不存在的文件、文件名 300 字节(超过文件系统的 255 字节上限)。同一份代码,用 cargo feature log 切换是否打开 tower-http/tracing:

tower-http = { version = "=0.7.1", features = ["fs"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "fs"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }

[features]
log = ["tower-http/tracing"]
use http::Request;
use http_body_util::Empty;
use std::os::unix::fs::PermissionsExt;
use tower::ServiceExt;
use tower_http::services::ServeDir;

#[tokio::main(flavor = "current_thread")]
async fn main() {
  tracing_subscriber::fmt().with_ansi(false).without_time().init();
  let root = std::env::temp_dir().join("p09-fslog-root");
  tokio::fs::create_dir_all(&root).await.unwrap();
  let secret = root.join("locked.txt");
  tokio::fs::write(&secret, "x").await.unwrap();
  tokio::fs::set_permissions(&secret, std::fs::Permissions::from_mode(0o000)).await.unwrap();

  let long = format!("/{}.txt", "a".repeat(300));
  for path in ["/locked.txt", "/missing.txt", long.as_str()] {
    let req = Request::get(path).body(Empty::<bytes::Bytes>::new()).unwrap();
    let res = ServeDir::new(&root).oneshot(req).await.unwrap();
    let shown = if path.len() > 20 { format!("/aaa...({} bytes)", path.len()) } else { path.to_owned() };
    println!("GET {shown:<20} -> {}", res.status());
  }
  tokio::fs::set_permissions(&secret, std::fs::Permissions::from_mode(0o644)).await.unwrap();
  tokio::fs::remove_dir_all(&root).await.unwrap();
}

M2 Max(APFS)、rustc 1.99.0,cargo run 与 cargo run --features log 各跑 3 次,输出相同:

== fs
GET /locked.txt          -> 404 Not Found
GET /missing.txt         -> 404 Not Found
GET /aaa...(305 bytes)   -> 500 Internal Server Error
== fs + tracing
GET /locked.txt          -> 404 Not Found
GET /missing.txt         -> 404 Not Found
ERROR tower_http::services::fs::serve_dir: Failed to read file error=File name too long (os error 63)
GET /aaa...(305 bytes)   -> 500 Internal Server Error

(== fs 两行是手加的分隔。)

出处

serve_dir/mod.rs 第 546 行起,Service::call 把 try_call 的错误变成响应:

                let response = result.unwrap_or_else(|err| {
                    let status = if should_return_not_found(&err) {
                        StatusCode::NOT_FOUND
                    } else {
                        #[cfg(feature = "tracing")]
                        tracing::error!(error = %err, "Failed to read file");

                        StatusCode::INTERNAL_SERVER_ERROR
                    };
  • should_return_not_found 把 NotFound、PermissionDenied 和 unix 上的 ENOTDIR 都算 404。没有读权限的文件回 404,不打日志,和不存在一样,对外看不出「文件在,但读不了」。反过来,部署时文件权限设错了,看到的也只是 404,不是 500。
  • 其余错误回 500,日志只在 tracing feature 打开时存在。这个 feature 是可选依赖 tracing 留下的隐式 feature,0.6.9 起(#614)fs 不再顺带打开它,trace 和 catch-panic 会。所以「升级以后 500 的日志没了」和「同一个项目里另一个 crate 开了 trace,日志又回来了」都可能发生,取决于整个依赖树里有没有谁打开了它。
  • 文件名过长在 0.7.1 是 500;tag 之后的 #744(未发版)把 ENAMETOOLONG 也归到 404。任何人都能请求一个超长路径,所以 0.7.1 下它是一个外部可触发、又没有日志的 500。

怎么确认、怎么修

看自己的依赖树里 tower-http 有没有开 tracing:

cargo tree -e features -i tower-http | grep 'feature "tracing"'

上面的实验里,不带 --features log 时这条命令没有输出,带上以后输出一行 tower-http feature "tracing"。没有输出就是没开。修法是在自己的 Cargo.toml 里显式加上:

tower-http = { version = "0.7", features = ["fs", "tracing"] }

还要有一个 tracing subscriber 在收 ERROR 级别。另一个办法是不依赖它的日志:在 ServeDir 外面套一层看状态码的中间件(比如 TraceLayer,它默认把 5xx 记成失败),至少知道发生过 500。

小结

  • ServeDir 只有 500 那一支打日志,而且要 tracing feature;0.6.9 起 fs 不再打开它。
  • 权限不足和不存在都回 404,不打日志;文件名过长在 0.7.1 回 500。
  • 用 cargo tree -e features 查,显式打开 tower-http/tracing,或者在外面用 TraceLayer 记 5xx。

💬 关联讨论

发起讨论
加载中...