建議設計成如此的JSON格式
{
"error": {
"code": "validation_error",
"message": "欄位 x 為必填",
"request_id": "..."
}
}
自訂錯誤型別與 IntoResponse 實作
Cargo.toml
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
thiserror = "1.0"
anyhow = { version = "1.0", default-features = false }
main.rs
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::Serialize;
use tracing::{error, info};
#[derive(Serialize)]
struct ErrorBody {
error: ErrorInfo,
}
#[derive(Serialize)]
struct ErrorInfo {
code: String,
message: String,
request_id: Option<String>,
}
// thiserror 會自動為 enum 實作 std::fmt::Display
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("validation failed: {0}")]
Validation(String),
#[error("unauthorized")]
Unauthorized,
#[error("forbidden")]
Forbidden,
#[error("not found")]
NotFound,
#[error("conflict: {0}")]
Conflict(String),
#[error("internal error")]
Internal(#[from] anyhow::Error),
}
impl AppError {
fn status_code(&self) -> StatusCode {
match self {
AppError::Validation(_) => StatusCode::BAD_REQUEST,
AppError::Unauthorized => StatusCode::UNAUTHORIZED,
AppError::Forbidden => StatusCode::FORBIDDEN,
AppError::NotFound => StatusCode::NOT_FOUND,
AppError::Conflict(_) => StatusCode::CONFLICT,
AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_code(&self) -> &'static str {
match self {
AppError::Validation(_) => "validation_error",
AppError::Unauthorized => "unauthorized",
AppError::Forbidden => "forbidden",
AppError::NotFound => "not_found",
AppError::Conflict(_) => "conflict",
AppError::Internal(_) => "internal_error",
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let request_id: Option<String> = None;
match &self {
AppError::Internal(e) => {
error!(error = %e, "internal error");
}
other => {
info!(error = %other.to_string(), "handled error");
}
}
let body = ErrorBody {
error: ErrorInfo {
code: self.error_code().to_string(),
message: match &self {
AppError::Internal(_) => "internal server error".to_string(),
AppError::Validation(msg) => msg.clone(),
_ => self.to_string(),
},
request_id,
},
};
let status = self.status_code();
let json = axum::Json(body);
(status, json).into_response()
}
}
use std::net::SocketAddr;
use tracing_subscriber;
#[derive(Serialize)]
struct HelloBody {
message: &'static str,
}
#[tokio::main]
async fn main() {
// 初始化 tracing
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let app = Router::new()
.route("/", get(root_handler))
.route("/error", get(error_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app)
.await
.unwrap();
}
async fn root_handler() -> Json<HelloBody> {
Json(HelloBody {
message: "Hello, world!",
})
}
async fn error_handler() -> Result<Json<HelloBody>, AppError> {
Err(AppError::Validation("invalid input example".to_string()))
}