第七章:错误处理与统一响应

博客v1.0系列教程(Rust)博客 v1.0 系列教程 (Rust)

7.1 错误码定义

// src/common/error.rs
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;

#[derive(Debug, Clone, Copy, Serialize)]
pub struct ErrorCode(pub i32);

impl ErrorCode {
    pub const SUCCESS: Self = Self(0);
    pub const NOT_FOUND: Self = Self(1001);
    pub const UNAUTHORIZED: Self = Self(1002);
    pub const FORBIDDEN: Self = Self(1003);
    pub const BAD_REQUEST: Self = Self(1004);
    pub const VALIDATION_ERROR: Self = Self(1005);
    pub const USER_EXISTS: Self = Self(2001);
    pub const USER_BLACKLISTED: Self = Self(2004);
    pub const DATABASE_ERROR: Self = Self(5001);
    pub const INTERNAL_ERROR: Self = Self(5000);
}

7.2 统一响应格式

#[derive(Debug, Serialize)]
pub struct ApiResult<T: Serialize> {
    pub code: i32,
    pub message: String,
    pub data: Option<T>,
}

impl<T: Serialize> ApiResult<T> {
    pub fn success(data: T) -> Self {
        ApiResult {
            code: ErrorCode::SUCCESS.0,
            message: "success".into(),
            data: Some(data),
        }
    }

    pub fn error(code: ErrorCode, message: &str) -> Self {
        ApiResult {
            code: code.0,
            message: message.into(),
            data: None,
        }
    }
}

7.3 自定义错误类型

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Not found: {0}")]
    NotFound(String),

    #[error("Unauthorized: {0}")]
    Unauthorized(String),

    #[error("Forbidden")]
    Forbidden,

    #[error("Bad request: {0}")]
    BadRequest(String),

    #[error("Validation error: {0}")]
    Validation(String),

    #[error("User already exists")]
    UserAlreadyExists,

    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("JWT error: {0}")]
    Jwt(#[from] jsonwebtoken::errors::Error),

    #[error("BCrypt error: {0}")]
    Bcrypt(#[from] bcrypt::BcryptError),

    #[error("Internal error: {0}")]
    Internal(String),
}

7.4 转换为 HTTP 响应

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, code, message) = match &self {
            AppError::NotFound(msg) => (
                StatusCode::NOT_FOUND,
                ErrorCode::NOT_FOUND,
                msg.clone(),
            ),
            AppError::Unauthorized(msg) => (
                StatusCode::UNAUTHORIZED,
                ErrorCode::UNAUTHORIZED,
                msg.clone(),
            ),
            AppError::Forbidden => (
                StatusCode::FORBIDDEN,
                ErrorCode::FORBIDDEN,
                "Forbidden".into(),
            ),
            AppError::BadRequest(msg) => (
                StatusCode::BAD_REQUEST,
                ErrorCode::BAD_REQUEST,
                msg.clone(),
            ),
            AppError::Validation(msg) => (
                StatusCode::UNPROCESSABLE_ENTITY,
                ErrorCode::VALIDATION_ERROR,
                msg.clone(),
            ),
            AppError::UserAlreadyExists => (
                StatusCode::CONFLICT,
                ErrorCode::USER_EXISTS,
                "User already exists".into(),
            ),
            AppError::Database(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                ErrorCode::DATABASE_ERROR,
                "Database error".into(),
            ),
            _ => (
                StatusCode::INTERNAL_SERVER_ERROR,
                ErrorCode::INTERNAL_ERROR,
                "Internal server error".into(),
            ),
        };

        (
            status,
            Json(ApiResult::<()>::error(code, &message)),
        ).into_response()
    }
}

7.5 全局异常捕获

pub async fn handle_panic(
    err: Box<dyn std::any::Any + Send + 'static>,
) -> Response {
    let message = if let Some(s) = err.downcast_ref::<&str>() {
        s.to_string()
    } else if let Some(s) = err.downcast_ref::<String>() {
        s.clone()
    } else {
        "Unknown panic".into()
    };

    tracing::error!("Panic: {}", message);

    (
        StatusCode::INTERNAL_SERVER_ERROR,
        Json(ApiResult::<()>::error(
            ErrorCode::INTERNAL_ERROR,
            "Internal server error",
        )),
    ).into_response()
}

7.6 在服务层使用

// src/services/article_service.rs
impl ArticleService {
    pub async fn get_article(&self, id: i64) -> Result<Article, AppError> {
        let article = sqlx::query_as::<_, Article>(
            "SELECT * FROM article_models WHERE id = $1"
        )
        .bind(id)
        .fetch_optional(&self.pool)
        .await?
        .ok_or_else(|| AppError::NotFound("Article not found".into()))?;

        Ok(article)
    }

    pub async fn create_article(&self, dto: CreateArticleDto) -> Result<Article, AppError> {
        // 参数验证
        if dto.title.trim().is_empty() {
            return Err(AppError::Validation("Title cannot be empty".into()));
        }

        let article = sqlx::query_as::<_, Article>(
            "INSERT INTO article_models (title, content, category)
             VALUES ($1, $2, $3) RETURNING *"
        )
        .bind(&dto.title)
        .bind(&dto.content)
        .bind(&dto.category)
        .fetch_one(&self.pool)
        .await?;

        Ok(article)
    }
}

7.7 端点中使用

async fn get_article_handler(
    State(pool): State<PgPool>,
    Path(id): Path<i64>,
) -> Result<Json<ApiResult<Article>>, AppError> {
    let service = ArticleService::new(pool);
    let article = service.get_article(id).await?;
    Ok(Json(ApiResult::success(article)))
}

下一章将实现文件上传与中间件。

rusterror-handlingthiserroraxum