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,日志只在
tracingfeature 打开时存在。这个 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 那一支打日志,而且要tracingfeature;0.6.9 起fs不再打开它。- 权限不足和不存在都回 404,不打日志;文件名过长在 0.7.1 回 500。
- 用
cargo tree -e features查,显式打开tower-http/tracing,或者在外面用TraceLayer记 5xx。
💬 关联讨论
发起讨论加载中...