Table of contents
Open Table of contents
全局图
错误来源 处理方式 最终结果
─────────────────────────────────────────────────────────────────────
请求格式不合法 Extractor Rejection 400/401/415
(无效 JSON, 缺少 header) → handler 不执行 自动处理
业务逻辑错误 自定义 AppError 404/409/422
(用户不存在, 余额不足) → handler 返回 Err 你来决定状态码
基础设施错误 anyhow + context 500
(数据库挂了, Redis 超时) → ? 自动传播 记日志,客户端只看到 500
意料之外的 panic catch_unwind / tower layer 500
(数组越界, unwrap 失败) → 不应该发生 记日志,返回 500
分层设计
┌─────────────────────────────────────────────────────────────────┐
│ 第一层: Extractor Rejection(axum 自动处理) │
│ │
│ 请求格式不对 → extractor 返回 Rejection → 4xx │
│ 你不需要写任何代码 │
├─────────────────────────────────────────────────────────────────┤
│ 第二层: 业务错误(你定义 AppError 枚举) │
│ │
│ "用户不存在" "库存不足" "重复操作" → 对应具体的 HTTP 状态码 │
│ 客户端根据错误类型决定怎么处理 │
├─────────────────────────────────────────────────────────────────┤
│ 第三层: 基础设施错误(anyhow 兜底) │
│ │
│ 数据库连接失败、Redis 超时、序列化错误 → 统一 500 │
│ 客户端不需要知道细节,日志里记录完整错误链 │
├─────────────────────────────────────────────────────────────────┤
│ 第四层: Panic(理论上不该发生) │
│ │
│ catch_unwind 兜住 → 返回 500 → 不崩整个服务 │
└─────────────────────────────────────────────────────────────────┘
具体实现
第一层:Extractor Rejection(零代码)
axum 自动处理,你什么都不需要做:
// 客户端发了无效 JSON → 400 Bad Request
// 客户端没带 Content-Type → 415 Unsupported Media Type
// Path 参数不是合法数字 → 400 Bad Request
async fn handler(Path(id): Path<u32>, Json(body): Json<CreateItem>) -> ... {
// 走到这里时参数一定是合法的
}
第二层:业务错误(AppError 枚举)
use axum::{http::StatusCode, response::{IntoResponse, Json}};
use serde_json::json;
#[derive(Debug)]
enum AppError {
// 客户端错误 — 客户端可以根据类型做不同处理
NotFound(String),
Conflict(String),
Forbidden(String),
BadRequest(String),
// 服务端错误 — 客户端只知道"服务器出错了"
Internal(anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, error_code, message) = match &self {
AppError::NotFound(msg) =>
(StatusCode::NOT_FOUND, "NOT_FOUND", msg.clone()),
AppError::Conflict(msg) =>
(StatusCode::CONFLICT, "CONFLICT", msg.clone()),
AppError::Forbidden(msg) =>
(StatusCode::FORBIDDEN, "FORBIDDEN", msg.clone()),
AppError::BadRequest(msg) =>
(StatusCode::BAD_REQUEST, "BAD_REQUEST", msg.clone()),
AppError::Internal(err) => {
// 内部错误:日志记完整信息,客户端只看到通用消息
tracing::error!("{:#}", err);
(StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"internal server error".to_string())
}
};
(status, Json(json!({
"error": {
"code": error_code,
"message": message,
}
}))).into_response()
}
}
第三层:基础设施错误用 From 接住
use anyhow::Context;
// 数据库错误 → 自动转成 AppError::Internal
impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound =>
AppError::NotFound("resource not found".into()),
other =>
AppError::Internal(other.into()),
}
}
}
// Redis 错误 → 统一 Internal
impl From<redis::RedisError> for AppError {
fn from(e: redis::RedisError) -> Self {
AppError::Internal(e.into())
}
}
也可以用 thiserror 自动生成,但手写 From 能做更细粒度的映射(比如 RowNotFound → 404 而不是 500)。
Handler 里的使用
async fn get_player(
State(state): State<AppState>,
Path(player_id): Path<u32>,
) -> Result<Json<Player>, AppError> {
// ? 自动转换:sqlx::Error::RowNotFound → AppError::NotFound → 404
let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id = $1")
.bind(player_id as i32)
.fetch_one(&state.db)
.await?;
Ok(Json(player))
}
async fn create_item(
State(state): State<AppState>,
Path(player_id): Path<u32>,
Json(body): Json<CreateItem>,
) -> Result<(StatusCode, Json<Item>), AppError> {
// 业务校验:手动返回具体错误
let player = get_player_or_404(&state.db, player_id).await?;
if player.inventory_full() {
return Err(AppError::BadRequest("inventory is full".into()));
}
if body.quantity <= 0 {
return Err(AppError::BadRequest("quantity must be positive".into()));
}
// 数据库操作:? 自动把 sqlx::Error 转成 AppError
let item = sqlx::query_as::<_, Item>(
"INSERT INTO items (player_id, name, quantity) VALUES ($1, $2, $3) RETURNING *"
)
.bind(player_id as i32)
.bind(&body.name)
.bind(body.quantity)
.fetch_one(&state.db)
.await
.context("failed to insert item")?; // context 添加上下文信息到日志
Ok((StatusCode::CREATED, Json(item)))
}
辅助函数也返回 AppError
async fn get_player_or_404(db: &PgPool, id: u32) -> Result<Player, AppError> {
sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id = $1")
.bind(id as i32)
.fetch_optional(db)
.await? // sqlx::Error → AppError::Internal
.ok_or_else(|| AppError::NotFound(format!("player {} not found", id)))
}
async fn verify_token(redis: &redis::Client, token: &str) -> Result<u32, AppError> {
let mut conn = redis.get_multiplexed_async_connection()
.await
.context("failed to connect to redis")?; // redis::Error → anyhow → Internal
let player_id: Option<u32> = conn.get(format!("session:{}", token)).await?;
player_id.ok_or(AppError::Forbidden("invalid or expired token".into()))
}
第四层:Panic 兜底(可选)
use tower_http::catch_panic::CatchPanicLayer;
let app = Router::new()
.route("/players", get(list_players))
.layer(CatchPanicLayer::new()); // 某个 handler panic 了 → 返回 500,不崩服务
响应格式统一
所有错误都返回统一的 JSON 结构:
// 成功
{
"id": 1001,
"name": "AK-47",
"quantity": 1
}
// 失败
{
"error": {
"code": "NOT_FOUND",
"message": "player 42 not found"
}
}
客户端判断逻辑:
HTTP 2xx → 解析 body 为业务数据
HTTP 4xx/5xx → 解析 body 为 error 结构,读 code 字段做分支处理
错误处理决策流程
发生了什么?
│
├─ 请求格式不对(JSON 语法错、参数类型错)
│ → extractor 自动处理,你不管
│
├─ 业务规则不满足(用户不存在、余额不足、无权限)
│ → return Err(AppError::NotFound/Conflict/Forbidden/BadRequest)
│ → 带有意义的 error code + message,客户端需要知道
│
├─ 基础设施出问题(数据库超时、Redis 挂了、网络错误)
│ → ? 自动传播 → From 转成 AppError::Internal
│ → 日志记完整错误链(含 context)
│ → 客户端只看到 500 "internal server error"
│
└─ 代码 bug(unwrap panic、数组越界)
→ CatchPanicLayer 兜住 → 500
→ 理论上不应该走到这里,修 bug
完整的 AppError 模块
// src/error.rs
use axum::{http::StatusCode, response::{IntoResponse, Json}};
use serde_json::json;
#[derive(Debug)]
pub enum AppError {
NotFound(String),
BadRequest(String),
Conflict(String),
Forbidden(String),
Internal(anyhow::Error),
}
impl AppError {
pub fn not_found(msg: impl Into<String>) -> Self {
Self::NotFound(msg.into())
}
pub fn bad_request(msg: impl Into<String>) -> Self {
Self::BadRequest(msg.into())
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self::Forbidden(msg.into())
}
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, code, message) = match &self {
Self::NotFound(msg) => (StatusCode::NOT_FOUND, "NOT_FOUND", msg.as_str()),
Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg.as_str()),
Self::Conflict(msg) => (StatusCode::CONFLICT, "CONFLICT", msg.as_str()),
Self::Forbidden(msg) => (StatusCode::FORBIDDEN, "FORBIDDEN", msg.as_str()),
Self::Internal(err) => {
tracing::error!("{:#}", err);
(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "internal server error")
}
};
(status, Json(json!({
"error": { "code": code, "message": message }
}))).into_response()
}
}
// 基础设施错误自动转换
impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => Self::NotFound("not found".into()),
other => Self::Internal(other.into()),
}
}
}
impl From<redis::RedisError> for AppError {
fn from(e: redis::RedisError) -> Self {
Self::Internal(e.into())
}
}
// 兜底:任何 anyhow::Error → Internal
impl From<anyhow::Error> for AppError {
fn from(e: anyhow::Error) -> Self {
Self::Internal(e)
}
}
Handler 里只需要 ? 和偶尔的 return Err(AppError::not_found(...)),其他全部自动。