Rust in Everything
基于 dioxus 0.7.10

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。

💬 关联讨论

发起讨论
加载中...