Axum 中的错误处理
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 实现了 Into 的 Result 上使用 ?。
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 | 快速原型或脚本 |
推荐的大多数应用的做法
-
定义一个中心错误类型
#[derive(Debug)] enum AppError { NotFound, InvalidInput(String), Internal(String), // …other variants } -
为其实现
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() } } -
转换第三方错误
impl From<LibError> for AppError { fn from(err: LibError) -> Self { // map `LibError` variants to `AppError` variants AppError::Internal(err.to_string()) } } -
在处理函数中使用
Resultasync fn handler() -> Result { let data = some_lib_call().await?; // `?` works because of `From` Ok(Json(data)) }
就这样。没有隐藏的 panic,只有显式、类型化的错误路径。