跳转到正文
zeno's blog
返回

Rust Web:Axum 返回值-IntoResponse 与错误处理

专题: Rust Web

Table of contents

Open Table of contents

Handler 返回值的唯一要求

实现 IntoResponse trait:

trait IntoResponse {
    fn into_response(self) -> Response;  // 把自己变成 HTTP Response
}

内置的 IntoResponse 实现

// 字符串 → 200 + text/plain
async fn a() -> &'static str { "hello" }
async fn b() -> String { "hello".to_string() }

// Json → 200 + application/json
async fn c() -> Json<Player> { Json(player) }

// 状态码
async fn d() -> StatusCode { StatusCode::NOT_FOUND }

// 元组:(状态码, 内容)
async fn e() -> (StatusCode, String) {
    (StatusCode::CREATED, "done".into())
}

// 元组:(状态码, 响应头, 内容)
async fn f() -> (StatusCode, [(&str, &str); 1], String) {
    (StatusCode::OK, [("X-Custom", "value")], "body".into())
}

// Result<T, E>:T 和 E 都必须实现 IntoResponse
async fn g() -> Result<Json<Player>, StatusCode> { ... }

自定义类型实现 IntoResponse

struct HtmlPage(String);

impl IntoResponse for HtmlPage {
    fn into_response(self) -> Response {
        (
            StatusCode::OK,
            [("Content-Type", "text/html")],
            self.0,
        ).into_response()
    }
}

async fn page() -> HtmlPage { HtmlPage("<h1>Hi</h1>".into()) }

Result 返回值与 ? 操作符

问题:为什么要用 Result 做返回值?

为了在 handler 里用 ? 操作符。没有 Result 返回值,? 没地方 return Err。

问题:Result<Json<Player>, StatusCode> 能配合 ? 吗?

几乎不能。因为 ? 需要通过 From trait 把错误类型转成 StatusCode,而没有任何库的错误类型实现了这个转换:

async fn handler(State(db): State<PgPool>) -> Result<Json<Player>, StatusCode> {
    let player = sqlx::query_as("SELECT ...")
        .fetch_one(&db)
        .await?;  // 编译错误!没有 From<sqlx::Error> for StatusCode

    Ok(Json(player))
}

Result<_, StatusCode> 只能手动构造 Err:

async fn handler() -> Result<Json<Player>, StatusCode> {
    if some_condition {
        return Err(StatusCode::NOT_FOUND);  // 只能手动
    }
    Ok(Json(player))
}

实际做法:自定义错误类型

要让 ? 能用,需要一个同时实现了 IntoResponseFrom<各种错误> 的类型

struct AppError(anyhow::Error);

// 让 ? 能用:任何错误 → AppError
impl<E: Into<anyhow::Error>> From<E> for AppError {
    fn from(err: E) -> Self {
        AppError(err.into())
    }
}

// 让 axum 能用:AppError → HTTP 响应
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response()
    }
}

现在 handler 里所有 ? 都能用了:

async fn handler(State(db): State<PgPool>) -> Result<Json<Player>, AppError> {
    let player = sqlx::query_as("SELECT ...")
        .fetch_one(&db)
        .await?;            // sqlx::Error → anyhow::Error → AppError ✓

    let token = generate_jwt(&player)?;  // jwt::Error → anyhow::Error → AppError ✓

    cache_session(&token).await?;        // redis::Error → anyhow::Error → AppError ✓

    Ok(Json(player))
}

? 的完整链路

handler 返回 Result<T, E>

  成功路径:
    Ok(Json(player))
      → axum 调 Json(player).into_response()
      → 200 + application/json + 序列化的 player

  失败路径(? 触发):
    sqlx::Error 发生
      → ? 调用 From::from(sqlx_error)
      → sqlx::Error → anyhow::Error → AppError
      → return Err(AppError(...))
      → axum 调 AppError.into_response()
      → 500 + 错误信息

更精细的错误映射

上面的方案把所有错误都返回 500。实际项目中可以区分错误类型返回不同状态码:

enum AppError {
    NotFound(String),
    BadRequest(String),
    Internal(anyhow::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            AppError::NotFound(msg) =>
                (StatusCode::NOT_FOUND, msg).into_response(),
            AppError::BadRequest(msg) =>
                (StatusCode::BAD_REQUEST, msg).into_response(),
            AppError::Internal(err) =>
                (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
        }
    }
}

impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self {
        match e {
            sqlx::Error::RowNotFound => AppError::NotFound("not found".into()),
            other => AppError::Internal(other.into()),
        }
    }
}

Extractor 与 IntoResponse 的对称设计

请求进来                                         响应出去
   │                                                ▲
   ▼                                                │
Extractor                                     IntoResponse
(FromRequest trait)                           (IntoResponse trait)
"怎么从请求中取数据"                           "怎么把结果变成响应"
   │                                                ▲
   ▼                                                │
   └──► handler(提取的参数) -> 返回值 ──────────────┘
              你的业务逻辑

两头都靠 trait 定义行为,中间是你的业务逻辑。


分享这篇文章:

上一篇
Rust Web:一个请求的完整生命周期
下一篇
Rust Web:Axum HTTP Server 指南