15.5 配置管理、错误处理与中间件
一个生产级 Web 服务不仅需要正确实现业务逻辑,还需具备良好的配置灵活性、统一的错误处理机制和可扩展的中间件支持。本节将围绕这三个方面,完善我们在前几节搭建的 API 项目,使其更健壮、可观测且易于运维。
配置管理
应用配置应通过环境变量或配置文件注入,避免硬编码。我们使用 config crate 结合 serde 实现分层配置加载(默认值 → 环境变量覆盖)。
首先定义配置结构体(src/config.rs):
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct AppConfig {
#[serde(default = "default_server_port")]
pub server_port: u16,
#[serde(default)]
pub database_url: String,
}
fn default_server_port() -> u16 {
3000
}
impl AppConfig {
pub fn from_env() -> Result<Self, config::ConfigError> {
let mut cfg = config::Config::new();
cfg.merge(config::Environment::new())?;
cfg.try_into()
}
}
在 main.rs 中加载:
let config = AppConfig::from_env().expect("Failed to load config");
let listener = TcpListener::bind(("0.0.0.0", config.server_port)).await?;
现在可通过 DATABASE_URL=postgres://... SERVER_PORT=8080 cargo run 动态配置。
统一错误处理
Rust 的 Result 类型天然支持错误传递,但 Web 框架需要将内部错误转换为 HTTP 响应。我们定义统一的 AppError 类型,并实现 IntoResponse(Axum)或 ResponseError(Actix-web)。
在 src/error.rs 中:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use sqlx::Error as SqlxError;
#[derive(Debug)]
pub enum AppError {
Sqlx(SqlxError),
NotFound,
ValidationError(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::Sqlx(e) => {
tracing::error!(error = %e, "Database error");
(StatusCode::INTERNAL_SERVER_ERROR, "Database error")
}
AppError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
// 为常见错误类型实现 From,简化 ? 操作符使用
impl From<SqlxError> for AppError {
fn from(err: SqlxError) -> Self {
match &err {
SqlxError::RowNotFound => AppError::NotFound,
_ => AppError::Sqlx(err),
}
}
}
这样,在 handler 中只需返回 Result<..., AppError>,任何 sqlx::Error 都会自动转为合适的 HTTP 响应。
中间件扩展
中间件用于横切关注点,如请求日志、CORS、认证等。Axum 基于 tower,可直接使用其生态中间件。
自定义请求日志中间件
虽然 tower-http 提供了 TraceLayer,但我们可以实现更定制化的日志:
use std::task::{Context, Poll};
use axum::{
body::Body,
http::{Request, Response},
middleware::Next,
};
use futures::FutureExt;
pub async fn log_requests<B>(
req: Request<B>,
next: Next<B>,
) -> Response {
let start = std::time::Instant::now();
let method = req.method().clone();
let uri = req.uri().clone();
let response = next.run(req).await;
let duration = start.elapsed();
tracing::info!(
method = %method,
uri = %uri,
status = response.status().as_u16(),
duration_ms = duration.as_millis(),
"request completed"
);
response
}
注册中间件:
use axum::middleware;
let app = Router::new()
.route("/tasks", post(create_task))
.layer(middleware::from_fn(log_requests));
启用 CORS
对于前端调用,通常需启用 CORS:
[dependencies]
tower-http = { version = "0.5", features = ["cors"] }
use tower_http::cors::CorsLayer;
let cors = CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods([axum::http::Method::GET, axum::http::Method::POST]);
let app = Router::new()
.route(...)
.layer(cors)
.layer(middleware::from_fn(log_requests));
小结
通过引入配置管理,应用变得可部署于不同环境;统一错误处理提升了 API 的一致性和可调试性;中间件机制则让横切逻辑模块化、可复用。这三者共同构成了生产就绪服务的基础骨架。至此,我们已构建出一个结构清晰、可观测、可配置且具备完整 CRUD 能力的 Rust Web API 服务。后续可根据需求进一步集成认证、限流、健康检查等功能,但核心工程实践已在本章体现。