server function 的错误都是 500
主线位置:调用主线的最后一段。函数体返回之后,服务端一半用
MakeAxumError把错误编成响应,状态码在这里定;客户端一半读状态码和ErrorPayload,解回错误类型。机制在 server function 的「ServerFnError 与状态码」一节,本页只讲怎么拿到想要的状态码。
flowchart TB
ret["函数返回 Err"] --> ty{"错误类型"}
ty -->|ServerFnError| sfe["ServerError 的 code"]
ty -->|"AsStatusCode 自定义"| own["as_status_code"]
ty -->|"HttpError 或 StatusCode"| raw["原样"]
ty -->|"dioxus::Result 或 anyhow"| fixed["固定 500"]
decode["参数解码失败"] --> fixed
实验应用 pit2-fs:dioxus = "=0.7.10"(router、fullstack),dx build --platform web --fullstack(dx 0.7.9)的 debug 构建,IP=127.0.0.1 PORT=18820 ./server。M2 Max、macOS 27、rustc 1.99.0。curl 输出去掉了 date、vary、content-length 几行。
现象
几种常见写法:
// copied from upstream examples/07-fullstack/handling_errors.rs
#[get("/api/throws-http-error")]
pub async fn throws_http_error() -> Result<()> {
HttpError::bad_request("Bad request example")?;
Ok(())
}
#[get("/api/need-login")]
pub async fn need_login() -> Result<String, ServerFnError> {
Err(ServerFnError::new("please log in"))
}
#[post("/api/double")]
pub async fn double(n: i32) -> Result<i32, ServerFnError> {
Ok(n * 2)
}
Result<()> 是 prelude 里的 dioxus::Result,错误类型是 CapturedError。第一个函数原样抄自 dioxus 仓库的示例,它上面的文档注释写的是「Demonstrates returning an explicit HTTP error (400 Bad Request) using HttpError」。
### curl -X POST /api/double -d {"n":"x"}
HTTP/1.1 500 Internal Server Error
content-type: application/json
{"error":"error deserializing server function results: invalid type: string \"x\", expected i32 at line 1 column 8"}
### curl /api/throws-http-error
HTTP/1.1 500 Internal Server Error
content-type: application/json
{"message":"Bad request example","code":400}
### curl /api/need-login
HTTP/1.1 500 Internal Server Error
content-type: application/json
{"message":"error running server function: please log in (details: None)","code":500,"data":{"ServerError":{"message":"please log in","code":500}}}
- 客户端传错了参数,是 500,不是 400。
dioxus::Result里的HttpError::bad_request:正文的code是 400,状态行是 500。示例注释说的 400 只在正文里。ServerFnError::new永远是 500,不管消息写的是什么。
原生客户端(pit2-client,不开 server feature,set_server_url 指向同一个服务端)解出来的是:
throws_http_error() = Err(CapturedError(error running server function: Bad request example (details: None)))
need_login() = Err(ServerError { message: "please log in", code: 500, details: None })
400 藏在 CapturedError 里面,要 downcast 才拿得到。浏览器的网络面板、反向代理的访问日志、监控的 5xx 告警看到的都是 500:客户端的错误被记成了服务端故障。
出处
返回类型的错误部分选中 MakeAxumError 的哪一份实现,状态码就由哪一份决定:
| 错误类型 | 实现 | 状态码 |
|---|---|---|
ServerFnError、实现了 AsStatusCode 的自定义类型 | 第 665 行 | as_status_code();ServerFnError 只有 ServerError { code } 用 code |
CapturedError(dioxus::Result) | 第 692 行 | 转给 anyhow 那一份,或自己 downcast;最后都是 500 |
anyhow::Error | 第 754 行 | downcast 出 HttpError 时把它的状态填进正文的 code,状态行写死 500 |
StatusCode | 第 805 行 | 原样 |
HttpError | 第 828 行 | 原样 |
anyhow 那一份最后两行:
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
resp
ServerFnError::new 构造的是 ServerError { code: 500, .. }。参数解码失败发生在函数体之前,serde_json 的错误经 From 变成 ServerFnError::Deserialization,IntoResponse for ServerFnError 对 ServerError 以外的分支一律 500。
修法
想要哪个状态码,就让返回类型能带它。四种写法:
// 1. HttpError: the status is the status
#[get("/api/need-login-401")]
pub async fn need_login_401() -> Result<String, HttpError> {
HttpError::unauthorized("please log in")
}
// Option / Result / bool get .or_not_found(..), .or_unauthorized(..) ... from OrHttpError
#[get("/api/post/{id}")]
pub async fn get_post(id: u32) -> Result<String, HttpError> {
let title = (id == 1).then(|| "hello".to_string());
title.or_not_found("no such post")
}
// 2. a bare StatusCode
#[get("/api/teapot")]
pub async fn teapot() -> Result<String, StatusCode> {
Err(StatusCode::IM_A_TEAPOT)
}
// 3. your own error type: Serialize + Deserialize + From<ServerFnError> + AsStatusCode
#[derive(thiserror::Error, Debug, Serialize, Deserialize)]
pub enum PostError {
#[error("not logged in")]
NotLoggedIn,
#[error("no such post")]
NotFound,
#[error("internal error")]
Internal(#[from] ServerFnError),
}
impl AsStatusCode for PostError {
fn as_status_code(&self) -> StatusCode {
match self {
PostError::NotLoggedIn => StatusCode::UNAUTHORIZED,
PostError::NotFound => StatusCode::NOT_FOUND,
PostError::Internal(e) => e.as_status_code(),
}
}
}
#[get("/api/typed/{id}")]
pub async fn typed_post(id: u32) -> Result<String, PostError> {
match id {
0 => Err(PostError::NotLoggedIn),
1 => Ok("hello".to_string()),
_ => Err(PostError::NotFound),
}
}
// 4. ServerFnError with an explicit code:
// Err(ServerFnError::ServerError { message: "..".into(), code: 401, details: None })
AsStatusCode、StatusCode 从 dioxus::fullstack 引入,HttpError、OrHttpError 在 prelude 里。重跑:
### curl /api/need-login-401
HTTP/1.1 401 Unauthorized
content-type: application/json
{"message":"please log in","code":401}
### curl /api/post/2
HTTP/1.1 404 Not Found
content-type: application/json
{"message":"no such post","code":404}
### curl /api/teapot
HTTP/1.1 418 I'm a teapot
content-type: application/json
{"message":"418 I'm a teapot","code":418}
### curl /api/typed/0
HTTP/1.1 401 Unauthorized
content-type: application/json
{"message":"not logged in","code":401,"data":"NotLoggedIn"}
### curl /api/typed/2
HTTP/1.1 404 Not Found
content-type: application/json
{"message":"no such post","code":404,"data":"NotFound"}
客户端那边也拿到了类型:
need_login_401() = Err(HttpError { status: 401, message: Some("please log in") })
get_post(2) = Err(HttpError { status: 404, message: Some("no such post") })
typed_post(0) = Err(NotLoggedIn)
typed_post(2) = Err(NotFound)
自定义错误类型的好处是客户端可以 match,不用比较字符串;From<ServerFnError> 是必需的,网络错误、解码错误在客户端要能变成它(这里是 Internal 分支)。dioxus::Result / anyhow 适合「失败就是服务端的锅」的函数,它们的状态码在 0.7.10 里没有办法改。
参数解码失败的 500 改不掉:它发生在函数体之前,走的是框架的 IntoResponse for ServerFnError。不想让坏参数到达函数,就让参数类型宽一点(比如 String),在函数体里自己校验并返回 HttpError::bad_request。
怎么确认没中招
curl -s -o /dev/null -w '%{http_code}\n'对每个会失败的分支打一次,断言状态码。上面的/api/throws-http-error就是一个打出来是 500、正文却写着 400 的例子:只看正文的测试发现不了。- 搜返回类型是
Result<T>(prelude 的dioxus::Result)或anyhow::Result的 server function,看函数体里有没有HttpError::xxx(..)?或.or_xxx(..)?:这些在这类返回类型下状态行全是 500。 - 搜
ServerFnError::new(:每一处都是 500。需要区分「没登录」「不存在」的地方换成上面的写法。 - 本站的情况见本站的页面。
小结
- 状态码由错误类型选中的
MakeAxumError实现决定:HttpError、StatusCode原样;ServerFnError用ServerError.code;实现AsStatusCode的自定义类型用它的返回值。 dioxus::Result(CapturedError)和anyhow::Error状态行固定 500,里面的HttpError只把状态写进正文的code;上游示例的注释在这一点上写错了。ServerFnError::new是 500;参数解不出来也是 500,而且改不掉。- 要 401 / 404:返回
Result<T, HttpError>配or_unauthorized/or_not_found,或者给自己的错误类型实现AsStatusCode。