Axum 中的错误处理

发布: (2026年5月3日 GMT+8 01:06)
8 分钟阅读
原文: Dev.to

Source: Dev.to

Axum 是一个基于 Tokio 和 Hyper 构建的 Rust Web 框架。
它的错误处理模型简单且可预测:每个错误最终都必须转化为 HTTP 响应。实现此功能的关键特性是 IntoResponse。任何实现了 IntoResponse 的类型都可以从处理函数返回,Axum 会将其转换为 HTTP 响应。

1. 直接返回 StatusCode

最简单的错误信号方式是从处理函数返回一个 StatusCode

use axum::{
    extract::Path,
    http::StatusCode,
    routing::get,
    Router,
};

async fn get_user(Path(id): Path) -> Result {
    if id == 0 {
        return Err(StatusCode::NOT_FOUND);
    }
    Ok(format!("User #{}", id))
}

#[tokio::main]
async fn main() {
    // ✅ Use `{id}` in the route pattern
    let app = Router::new().route("/users/{id}", get(get_user));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}
  • 当处理函数返回 Ok 时,Axum 会发送 200 OK
  • 当它返回 Err(StatusCode) 时,Axum 会发送提供的状态码。

单独的 StatusCode 并不能说明为什么失败,因此在大多数情况下,你会希望返回更多信息。

2. 返回 (StatusCode, String) 元组

您可以返回一个包含状态码和纯文本消息的元组。该元组会自动实现 IntoResponse

use axum::{
    extract::Path,
    http::StatusCode,
    routing::post,
    Json,
    Router,
};
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
}

async fn create_user(
    Json(payload): Json,
) -> Result {
    if payload.username.is_empty() {
        return Err((
            StatusCode::BAD_REQUEST,
            "Username cannot be empty".to_string(),
        ));
    }

    Ok(format!("Created user: {}", payload.username))
}

3. 整个应用的单一错误枚举

在实际项目中,通常会定义一种覆盖所有可能失败模式的错误类型,并为其实现 IntoResponse

use axum::{
    extract::Path,
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::get,
    Router,
};

/// Application‑wide error type
enum AppError {
    NotFound(String),
    Unauthorized,
    InternalError(String),
}

/// Convert `AppError` into an HTTP response
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            AppError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                "You are not authorized".to_string(),
            ),
            AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
        };

        (status, message).into_response()
    }
}

// Example handler using the custom error type
async fn get_document(Path(id): Path) -> Result {
    if id == 99 {
        return Err(AppError::Unauthorized);
    }
    if id > 100 {
        return Err(AppError::NotFound(format!("Document {} not found", id)));
    }
    Ok(format!("Document #{}", id))
}

模式: 一个错误类型,一个定义其如何转换为响应的地方。

4. JSON 错误体

APIs 通常返回 JSON 错误负载,而不是纯文本。下面是一种常见的实现方式。

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::Serialize;

#[derive(Serialize)]
struct ErrorBody {
    error: String,
    code: u16,
}

enum ApiError {
    NotFound(String),
    BadRequest(String),
    Internal,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
            ApiError::Internal => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Something went wrong".to_string(),
            ),
        };

        let body = Json(ErrorBody {
            error: message,
            code: status.as_u16(),
        });

        (status, body).into_response()
    }
}

返回示例

{
  "error": "Document 999 not found",
  "code": 404
}

5. 使用 ?From 传播错误

在实际代码中,你通常会想使用 ? 运算符将底层库的错误向上传递。为你的 AppError 实现 From,这样转换会自动完成。

use std::num::ParseIntError;

enum AppError {
    BadRequest(String),
    Internal(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // Same implementation as before (omitted for brevity)
        unimplemented!()
    }
}

// Convert `ParseIntError` into `AppError`
impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        AppError::BadRequest(format!("Invalid number: {}", err))
    }
}

async fn parse_id(Path(raw): Path) -> Result {
    // `?` calls the `From` impl on failure
    let id: u32 = raw.parse()?;
    Ok(format!("Parsed id: {}", id))
}

raw.parse() 后面的 ? 会自动将 ParseIntError 转换为 AppError::BadRequest,因此你不需要手动进行转换。

6. 使用 anyhow 进行快速原型开发

当你不需要细粒度的错误类型时,anyhow crate 提供了一个方便的通用错误类型。

use anyhow::Context;
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};

/// Wrapper that implements `IntoResponse` for `anyhow::Error`
struct AnyhowError(anyhow::Error);

impl IntoResponse for AnyhowError {
    fn into_response(self) -> Response {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Internal error: {}", self.0),
        )
            .into_response()
    }
}

// Allow any error that can be turned into `anyhow::Error` to be converted
impl<E> From<E> for AnyhowError {
    fn from(err: E) -> Self {
        AnyhowError(err.into())
    }
}

async fn risky_handler() -> Result {
    let data = std::fs::read_to_string("config.txt")
        .context("Failed to read config file")?;
    Ok(data)
}

现在,你可以在任何 E 实现了 IntoResult 上使用 ?

7. 使用 HandleError 转换 Tower 服务错误

如果你使用返回自定义错误类型的 Tower 服务或中间件,Axum 的 HandleError 层可以让你将这些错误映射为响应。

use axum::{
    error_handling::HandleError,
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Router,
};
use std::convert::Infallible;
use std::time::Duration;
use tower::timeout::TimeoutLayer;

// Example service that may time out
async fn timeout_handler() -> Result {
    Ok("All good".to_string())
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route(
            "/",
            get(
                timeout_handler
                    .layer(TimeoutLayer::new(Duration::from_secs(1)))
                    .handle_error(|err| async move {
                        // Convert the timeout error into a response
                        (
                            StatusCode::REQUEST_TIMEOUT,
                            format!("Request timed out: {}", err),
                        )
                            .into_response()
                    }),
            ),
        );

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

HandleError 允许你为服务的错误类型自定义转换为 IntoResponse 实现,从而保持其余代码的简洁。

摘要

  • IntoResponse 是 Rust 错误与 HTTP 响应之间的桥梁。
  • 使用 StatusCode(StatusCode, String) 简单入手。
  • 对于大型应用,定义一个统一的错误枚举并一次性实现 IntoResponse
  • 为 API 使用者返回 JSON 响应体。
  • 使用 From 实现,使 ? 运算符自动传播错误。
  • 在不需要详细错误层次的原型开发中,anyhow 非常方便。
  • HandleError 可帮助转换来自 Tower 服务或中间件的错误。
  • 使用这些模式,你可以在任何 Axum 应用中构建健壮且易用的错误处理。

Tower & Axum 错误处理示例

use axum::{
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Router,
};
use std::convert::Infallible;

// A function that may fail
async fn might_fail() -> Result {
    Err("Something broke".to_string())
}

// Wrap the service to handle its errors
let service = tower::service_fn(|_req| async {
    Ok::("ok")
});

let handled = HandleError::new(service, |err: String| async move {
    (StatusCode::INTERNAL_SERVER_ERROR, err)
});

这更为高级,主要在集成第三方 Tower 组件时相关。

错误处理模式

模式适用场景
Result简单的、仅代码层面的错误
Result with (StatusCode, String)带有错误信息的错误
Custom enum + impl IntoResponse需要类型化错误的生产环境应用
impl IntoResponse returning Json返回 JSON 错误体的 API
impl From for AppError使用 ? 处理库错误
anyhow wrapper快速原型或脚本

推荐的大多数应用的做法

  1. 定义一个中心错误类型

    #[derive(Debug)]
    enum AppError {
        NotFound,
        InvalidInput(String),
        Internal(String),
        // …other variants
    }
  2. 为其实现 IntoResponse(通常返回 JSON 是最佳选择)

    impl IntoResponse for AppError {
        fn into_response(self) -> axum::response::Response {
            let (status, body) = match self {
                AppError::NotFound => (
                    StatusCode::NOT_FOUND,
                    json!({ "error": "resource not found" })
                ),
                AppError::InvalidInput(msg) => (
                    StatusCode::BAD_REQUEST,
                    json!({ "error": msg })
                ),
                AppError::Internal(msg) => (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": msg })
                ),
            };
            (status, Json(body)).into_response()
        }
    }
  3. 转换第三方错误

    impl From<LibError> for AppError {
        fn from(err: LibError) -> Self {
            // map `LibError` variants to `AppError` variants
            AppError::Internal(err.to_string())
        }
    }
  4. 在处理函数中使用 Result

    async fn handler() -> Result {
        let data = some_lib_call().await?; // `?` works because of `From`
        Ok(Json(data))
    }

就这样。没有隐藏的 panic,只有显式、类型化的错误路径。

0 浏览
Back to Blog

相关文章

阅读更多 »

Rust 正在参加 Outreachy

简介 Rust 项目一直在积极参与开源导师计划,建立了丰富的历史,其中包括 Google Summer of Code https://bl...