Обработка ошибок в Axum
Эффективная обработка ошибок является ключевым аспектом разработки надежных веб-приложений. Axum предоставляет гибкие механизмы для обработки и представления ошибок. В этом разделе рассмотрим общие подходы к обработке ошибок в Axum.
Содержание
- Основные концепции
- Простая обработка ошибок
- Типажи для обработки ошибок
- Централизованная обработка ошибок
- Обработка ошибок различных типов
- Логирование ошибок
- Лучшие практики
Основные концепции
В Axum обработка ошибок основана на нескольких ключевых концепциях:
- Возвращаемые значения из обработчиков - тип
Result<T, E>
, гдеE
реализуетIntoResponse
- Типаж
IntoResponse
- позволяет преобразовать ошибки в HTTP-ответы - Middleware для обработки ошибок - перехватывает и обрабатывает ошибки глобально
- Механизм отклонений (rejections) - обрабатывает ошибки в экстракторах
Простая обработка ошибок
Базовая обработка ошибок с использованием Result
:
use axum::{
routing::get,
http::StatusCode,
response::IntoResponse,
Json,
Router,
};
use serde_json::{json, Value};
use std::sync::Arc;
// Простой обработчик с обработкой ошибок
async fn get_user(
axum::extract::Path(user_id): axum::extract::Path<String>,
) -> Result<Json<Value>, (StatusCode, String)> {
// Имитация поиска пользователя
if user_id == "123" {
let user = json!({
"id": "123",
"name": "Иван",
"email": "ivan@example.com"
});
Ok(Json(user))
} else {
Err((StatusCode::NOT_FOUND, format!("Пользователь с ID {} не найден", user_id)))
}
}
// Обработчик с более сложным объектом ошибки
async fn create_post(
Json(payload): Json<Value>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
// Проверка данных
if payload.get("title").is_none() {
let error = json!({
"error": "Отсутствует обязательное поле",
"field": "title",
"code": "FIELD_REQUIRED"
});
return Err((StatusCode::BAD_REQUEST, Json(error)));
}
// Имитация создания поста
let post = json!({
"id": "456",
"title": payload["title"],
"created_at": "2023-01-01T12:00:00Z"
});
Ok(Json(post))
}
// Настройка маршрутов
let app = Router::new()
.route("/users/:user_id", get(get_user))
.route("/posts", axum::routing::post(create_post));
Типажи для обработки ошибок
Использование типажа IntoResponse
для создания собственных типов ошибок:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
// Определение перечисления ошибок приложения
#[derive(Debug, Error)]
enum AppError {
#[error("Ошибка базы данных: {0}")]
Database(#[from] sqlx::Error),
#[error("Ошибка валидации: {0}")]
Validation(String),
#[error("Ресурс не найден: {0}")]
NotFound(String),
#[error("Неавторизованный доступ: {0}")]
Unauthorized(String),
#[error("Внутренняя ошибка сервера: {0}")]
InternalError(String),
}
// Структура для JSON-представления ошибок
#[derive(Serialize)]
struct ErrorResponse {
status: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<serde_json::Value>,
}
// Реализация IntoResponse для AppError
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_response) = match &self {
AppError::Database(err) => {
println!("Ошибка базы данных: {:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error".to_string(),
message: "Ошибка базы данных".to_string(),
code: Some("DATABASE_ERROR".to_string()),
details: None,
},
)
},
AppError::Validation(message) => (
StatusCode::BAD_REQUEST,
ErrorResponse {
status: "error".to_string(),
message: message.clone(),
code: Some("VALIDATION_ERROR".to_string()),
details: None,
},
),
AppError::NotFound(resource) => (
StatusCode::NOT_FOUND,
ErrorResponse {
status: "error".to_string(),
message: format!("Ресурс не найден: {}", resource),
code: Some("RESOURCE_NOT_FOUND".to_string()),
details: None,
},
),
AppError::Unauthorized(message) => (
StatusCode::UNAUTHORIZED,
ErrorResponse {
status: "error".to_string(),
message: message.clone(),
code: Some("UNAUTHORIZED".to_string()),
details: None,
},
),
AppError::InternalError(message) => {
println!("Внутренняя ошибка: {}", message);
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error".to_string(),
message: "Внутренняя ошибка сервера".to_string(),
code: Some("INTERNAL_SERVER_ERROR".to_string()),
details: None,
},
)
},
};
(status, Json(error_response)).into_response()
}
}
// Пример использования в обработчике
async fn get_user_by_id(
Path(user_id): Path<String>,
State(db): State<DatabasePool>,
) -> Result<Json<User>, AppError> {
// Поиск пользователя в базе данных
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(user_id.clone())
.fetch_optional(&db)
.await
.map_err(AppError::Database)?;
// Проверка результата
match user {
Some(user) => Ok(Json(user)),
None => Err(AppError::NotFound(format!("Пользователь {}", user_id))),
}
}
Централизованная обработка ошибок
Для единообразной обработки ошибок можно использовать middleware:
use axum::{
http::{Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
Router,
};
use std::panic::{self, AssertUnwindSafe};
use std::sync::Arc;
// Middleware для обработки паник
async fn handle_panic_middleware<B>(
request: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
// Оборачиваем вызов следующего обработчика в catch_unwind
let result = AssertUnwindSafe(next.run(request)).catch_unwind().await;
match result {
Ok(response) => Ok(response),
Err(panic_error) => {
// Логирование паники
eprintln!("Паника в обработчике: {:?}", panic_error);
// Возвращение ответа 500
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
// Логгер ошибок
async fn error_logger<B>(
request: Request<B>,
next: Next<B>,
) -> Response {
let path = request.uri().path().to_string();
let method = request.method().clone();
// Выполнение запроса
let response = next.run(request).await;
// Проверка статуса ответа
if response.status().is_client_error() || response.status().is_server_error() {
eprintln!("Ошибка при обработке запроса {} {}: {}",
method, path, response.status());
}
response
}
// Применение middleware
let app = Router::new()
// Маршруты приложения
.route("/users/:id", get(get_user_by_id))
.route("/posts", post(create_post))
// Middleware для обработки ошибок
.layer(middleware::from_fn(error_logger))
.layer(middleware::from_fn(handle_panic_middleware));
Обработка ошибок различных типов
Обработка ошибок экстракторов и валидации:
use axum::{
extract::{Path, Query, Json, rejection::*},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Router,
};
use serde::Deserialize;
use validator::Validate;
// Параметры запроса с валидацией
#[derive(Debug, Deserialize, Validate)]
struct UserParams {
#[validate(length(min = 3, message = "имя должно содержать не менее 3 символов"))]
name: Option<String>,
#[validate(range(min = 1, max = 100, message = "возраст должен быть от 1 до 100"))]
age: Option<u8>,
}
// Обработчик с обработкой ошибок валидации
async fn search_users(
query: Query<UserParams>,
) -> Result<Json<Vec<User>>, AppError> {
// Валидация параметров
if let Err(err) = query.validate() {
return Err(AppError::Validation(format!("Ошибка валидации: {:?}", err)));
}
// Обработка запроса
let users = find_users(&query).await?;
Ok(Json(users))
}
// Обработка ошибок экстрактора JSON
async fn create_user(
json_result: Result<Json<CreateUser>, JsonRejection>,
) -> Response {
match json_result {
Ok(Json(payload)) => {
// Валидация данных
if let Err(err) = payload.validate() {
return AppError::Validation(format!("Ошибка валидации: {:?}", err))
.into_response();
}
// Создание пользователя
match create_user_in_db(&payload).await {
Ok(user) => Json(user).into_response(),
Err(err) => err.into_response(),
}
},
Err(err) => {
let message = match err {
JsonRejection::JsonDataError(err) => {
format!("Ошибка данных JSON: {}", err)
},
JsonRejection::JsonSyntaxError(err) => {
format!("Ошибка синтаксиса JSON: {}", err)
},
JsonRejection::MissingJsonContentType(err) => {
format!("Отсутствует Content-Type: {}", err)
},
_ => "Неизвестная ошибка при разборе JSON".to_string(),
};
AppError::Validation(message).into_response()
}
}
}
// Обработка других типов отклонений
async fn handle_rejection(rejection: BoxRejection) -> Response {
if let Some(err) = rejection.downcast_ref::<AxumQueryRejection>() {
return AppError::Validation(format!("Ошибка в параметрах запроса: {}", err))
.into_response();
}
if let Some(err) = rejection.downcast_ref::<AxumPathRejection>() {
return AppError::Validation(format!("Ошибка в параметрах пути: {}", err))
.into_response();
}
// Обработка других типов отклонений
// Если тип отклонения неизвестен
(
StatusCode::INTERNAL_SERVER_ERROR,
"Произошла внутренняя ошибка сервера".to_string(),
)
.into_response()
}
// Настройка маршрутов
let app = Router::new()
.route("/users", get(search_users).post(create_user))
// Обработчик отклонений по умолчанию
.fallback(handle_rejection);
Логирование ошибок
Интеграция с системами логирования для отслеживания ошибок:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Router,
};
use serde_json::json;
use tracing::{error, info, instrument, warn};
// Функция для логирования ошибок
fn log_error(err: &AppError, request_id: &str) {
match err {
AppError::Database(db_err) => {
error!(
request_id = %request_id,
error = %db_err,
"Ошибка базы данных"
);
},
AppError::Validation(message) => {
warn!(
request_id = %request_id,
message = %message,
"Ошибка валидации"
);
},
AppError::NotFound(resource) => {
info!(
request_id = %request_id,
resource = %resource,
"Ресурс не найден"
);
},
AppError::Unauthorized(message) => {
warn!(
request_id = %request_id,
message = %message,
"Неавторизованный доступ"
);
},
AppError::InternalError(message) => {
error!(
request_id = %request_id,
message = %message,
"Внутренняя ошибка сервера"
);
},
}
}
// Обновленная реализация IntoResponse для AppError с логированием
impl IntoResponse for AppError {
fn into_response(self) -> Response {
// Генерация ID запроса
let request_id = uuid::Uuid::new_v4().to_string();
// Логирование ошибки
log_error(&self, &request_id);
// Преобразование ошибки в ответ
let (status, error_message) = match &self {
// ... (как в предыдущем примере)
};
// Добавление ID запроса в ответ
let body = json!({
"status": "error",
"message": error_message,
"request_id": request_id,
});
(status, Json(body)).into_response()
}
}
// Обработчик с трассировкой
#[instrument(skip(db))]
async fn get_user(
Path(user_id): Path<String>,
State(db): State<DatabasePool>,
) -> Result<Json<User>, AppError> {
info!("Получение пользователя с ID: {}", user_id);
let user = db.get_user(&user_id).await
.map_err(|e| {
error!("Ошибка при получении пользователя: {:?}", e);
AppError::Database(e)
})?;
Ok(Json(user))
}
Лучшие практики
Разделение типов ошибок
- Создавайте разные типы ошибок для разных частей приложения
- Используйте перечисления для группировки связанных ошибок
rust// Ошибки базы данных enum DatabaseError { ConnectionFailed(String), QueryFailed(String), TransactionFailed(String), } // Ошибки бизнес-логики enum DomainError { InvalidOperation(String), ResourceNotFound(String), BusinessRuleViolation(String), }
Информативные сообщения об ошибках
- Предоставляйте понятные сообщения для пользователей
- Включайте технические детали только в логи, не в ответы
rustmatch db_error { DbError::ConnectionLost => { error!("Потеряно соединение с базой данных: {:?}", db_error); AppError::InternalError("Проблема с сервисом. Повторите попытку позже.".to_string()) } }
Контекстные ошибки
- Добавляйте контекст к ошибкам при их прохождении через слои приложения
- Используйте библиотеки вроде
anyhow
илиeyre
для обогащения ошибок
rustasync fn process_payment( payment: Payment, db: &Database, ) -> Result<(), anyhow::Error> { db.begin_transaction() .await .context("Не удалось начать транзакцию")?; db.update_balance(payment.account_id, payment.amount) .await .context(format!("Не удалось обновить баланс для аккаунта {}", payment.account_id))?; db.commit_transaction() .await .context("Не удалось завершить транзакцию")?; Ok(()) }
Безопасность ошибок
- Не раскрывайте внутренние детали в ошибках
- Скрывайте специфические сообщения от пользователей
rust// Плохо (раскрывает внутренние данные) Err(format!("Ошибка подключения к БД: {}: {}", db_host, db_error)) // Хорошо log_error!("Ошибка подключения к БД: {}: {}", db_host, db_error); Err(AppError::ServiceUnavailable("Сервис временно недоступен".to_string()))
Единообразная структура ответов
- Используйте последовательный формат для успешных и ошибочных ответов
- Стандартизируйте коды ошибок и сообщения
rust// Общая структура ответа #[derive(Serialize)] struct ApiResponse<T> { status: String, // "success" или "error" #[serde(skip_serializing_if = "Option::is_none")] data: Option<T>, #[serde(skip_serializing_if = "Option::is_none")] error: Option<ErrorDetails>, } #[derive(Serialize)] struct ErrorDetails { code: String, message: String, #[serde(skip_serializing_if = "Option::is_none")] details: Option<serde_json::Value>, } // Функция для создания успешного ответа fn success<T>(data: T) -> Json<ApiResponse<T>> { Json(ApiResponse { status: "success".to_string(), data: Some(data), error: None, }) } // Функция для создания ответа с ошибкой fn error(code: &str, message: &str) -> Json<ApiResponse<()>> { Json(ApiResponse { status: "error".to_string(), data: None, error: Some(ErrorDetails { code: code.to_string(), message: message.to_string(), details: None, }), }) }
Эффективная обработка ошибок делает приложение более надежным и удобным как для пользователей, так и для разработчиков. Axum предоставляет гибкие инструменты для создания системы обработки ошибок, которая может быть адаптирована под конкретные потребности вашего приложения.