Хендлеры
Хендлеры (обработчики) — это основные строительные блоки веб-приложений на Axum. Они отвечают за обработку входящих HTTP-запросов и формирование ответов. В этом разделе мы подробно рассмотрим, как создавать и использовать хендлеры, а также познакомимся с их возможностями и особенностями.
Что такое хендлер?
В Axum хендлер — это функция или тип, который реализует типаж (trait) Handler
. Хендлеры принимают данные из запроса через параметры (экстракторы) и возвращают результаты, которые могут быть преобразованы в HTTP-ответы.
use axum::{
Json,
response::IntoResponse,
http::StatusCode,
};
use serde::{Serialize, Deserialize};
// Простой хендлер, возвращающий текст
async fn hello_world() -> &'static str {
"Привет, мир!"
}
// Хендлер с параметрами и возвращаемым JSON
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
}
async fn create_user(
Json(payload): Json<CreateUser>,
) -> impl IntoResponse {
let user = User {
id: 42,
name: payload.name,
};
(StatusCode::CREATED, Json(user))
}
Сигнатура хендлеров
Базовый синтаксис
Хендлеры в Axum обычно имеют следующую структуру:
async fn handler_name(
// Параметры-экстракторы (0 или более)
) -> тип_возвращаемого_значения {
// Логика обработки
}
Почти всегда хендлеры должны быть асинхронными функциями (с ключевым словом async
), но есть исключения, которые мы рассмотрим позже.
Возвращаемые типы
Хендлеры могут возвращать различные типы, которые реализуют типаж IntoResponse
. Вот наиболее распространенные варианты:
// Строка - превращается в текстовый ответ с Content-Type: text/plain
async fn plain_text() -> &'static str {
"Это простой текстовый ответ"
}
// Статус-код - ответ без тела
async fn no_content() -> StatusCode {
StatusCode::NO_CONTENT
}
// Кортеж статуса и тела
async fn status_with_text() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Ресурс не найден")
}
// JSON-ответ
async fn json_response() -> Json<User> {
Json(User {
id: 1,
name: "Иван".to_string(),
})
}
// Кортеж статуса и JSON
async fn status_with_json() -> (StatusCode, Json<User>) {
let user = User {
id: 2,
name: "Мария".to_string(),
};
(StatusCode::CREATED, Json(user))
}
// Результат с обработкой ошибок
async fn result_handler() -> Result<Json<User>, StatusCode> {
if some_condition {
Ok(Json(User {
id: 3,
name: "Алексей".to_string(),
}))
} else {
Err(StatusCode::NOT_FOUND)
}
}
// Явная реализация IntoResponse
async fn custom_response() -> impl IntoResponse {
// Любая логика...
(
StatusCode::OK,
[
("X-Custom-Header", "custom-value"),
("Server", "Axum"),
],
Json(User { id: 4, name: "Елена".to_string() }),
)
}
Использование типажа IntoResponse
Типаж IntoResponse
является ключевым для системы ответов Axum. Множество типов уже реализуют этот типаж, включая:
- Строки (
&str
,String
) - Статус-коды (
StatusCode
) - Кортежи статуса и тела (
(StatusCode, T)
где T реализуетIntoResponse
) - JSON-обертка (
Json<T>
где T реализуетSerialize
) - Результаты (
Result<T, E>
где T и E реализуютIntoResponse
) - HTTP-ответы (
Response<Body>
) - Многие другие типы из стандартной библиотеки и крейтов
Вы также можете реализовать IntoResponse
для собственных типов:
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
};
enum AppError {
NotFound,
InvalidInput,
DatabaseError,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "Ресурс не найден"),
AppError::InvalidInput => (StatusCode::BAD_REQUEST, "Некорректные входные данные"),
AppError::DatabaseError => (StatusCode::INTERNAL_SERVER_ERROR, "Ошибка базы данных"),
};
(status, message).into_response()
}
}
// Теперь можно использовать AppError в хендлерах
async fn get_user(Path(id): Path<u64>) -> Result<Json<User>, AppError> {
match find_user(id).await {
Some(user) => Ok(Json(user)),
None => Err(AppError::NotFound),
}
}
Параметры хендлеров (экстракторы)
Хендлеры могут принимать данные из запроса через параметры, которые называются экстракторами. Они извлекают данные из различных частей HTTP-запроса.
async fn complex_handler(
// Путь запроса
Path(id): Path<u64>,
// Query-параметры
Query(params): Query<Params>,
// Тело запроса как JSON
Json(payload): Json<CreateItem>,
// Доступ к HTTP-заголовкам
headers: HeaderMap,
// Доступ к методу запроса
method: Method,
// Доступ к состоянию приложения
State(app_state): State<AppState>,
) -> impl IntoResponse {
// Обработка запроса...
}
Подробнее об экстракторах мы поговорим в следующем разделе.
Асинхронность в хендлерах
Хендлеры в Axum обычно объявляются как асинхронные функции с ключевым словом async
. Это позволяет эффективно обрабатывать множество запросов одновременно без блокирования потоков.
async fn async_handler() -> &'static str {
// Выполнение асинхронных операций
tokio::time::sleep(Duration::from_millis(100)).await;
"Готово!"
}
Выполнение блокирующих операций
Иногда вам может потребоваться выполнить операцию, которая блокирует поток выполнения (например, синхронный ввод-вывод или вычисления). В таких случаях используйте spawn_blocking
из Tokio:
use tokio::task;
async fn with_blocking_operation() -> String {
// Выполнение блокирующей операции в отдельном потоке
let result = task::spawn_blocking(|| {
// Эта функция выполняется в пуле потоков для блокирующих задач
std::thread::sleep(std::time::Duration::from_millis(100));
"Результат блокирующей операции".to_string()
}).await.unwrap();
result
}
Хендлеры-замыкания
Помимо именованных функций, вы можете использовать анонимные функции и замыкания как хендлеры:
use axum::{Router, routing::get};
let app = Router::new()
// Встроенное замыкание как хендлер
.route("/inline", get(|| async { "Привет из замыкания!" }))
// Замыкание с захватом переменных
.route("/counter", {
let counter = std::sync::atomic::AtomicU64::new(0);
get(move || async move {
let count = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
format!("Счетчик посещений: {}", count)
})
});
Это особенно полезно для простых обработчиков или случаев, когда нужно захватить значения из контекста.
Синхронные хендлеры
Хотя асинхронные хендлеры предпочтительнее, Axum также поддерживает синхронные хендлеры для простых случаев:
// Синхронный хендлер (без async)
fn sync_handler() -> &'static str {
"Я синхронный хендлер!"
}
let app = Router::new()
.route("/sync", get(sync_handler));
Синхронные хендлеры автоматически оборачиваются в асинхронный контекст, поэтому снаружи они ведут себя так же, как асинхронные.
Обработка ошибок
Axum предоставляет несколько подходов к обработке ошибок в хендлерах.
Использование Result
Самый простой способ — возвращать Result<T, E>
, где T
и E
реализуют IntoResponse
:
async fn get_user(Path(id): Path<u64>) -> Result<Json<User>, StatusCode> {
match find_user(id).await {
Some(user) => Ok(Json(user)),
None => Err(StatusCode::NOT_FOUND),
}
}
Кастомные типы ошибок
Для более сложных случаев можно создать собственные типы ошибок:
// Определение собственного типа ошибки
#[derive(Debug)]
enum ApiError {
NotFound,
BadRequest(String),
Internal(anyhow::Error),
}
// Реализация преобразования в HTTP-ответ
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "Ресурс не найден".into()),
ApiError::BadRequest(message) => (StatusCode::BAD_REQUEST, message),
ApiError::Internal(err) => {
// Логирование внутренней ошибки
tracing::error!("Внутренняя ошибка: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, "Внутренняя ошибка сервера".into())
}
};
// Формирование JSON-ответа с ошибкой
let body = Json(serde_json::json!({
"error": error_message,
}));
(status, body).into_response()
}
}
// Использование в хендлере
async fn create_user(
Json(payload): Json<CreateUser>,
) -> Result<Json<User>, ApiError> {
// Валидация входных данных
if payload.name.is_empty() {
return Err(ApiError::BadRequest("Имя не может быть пустым".into()));
}
// Попытка создания пользователя
match create_user_in_db(&payload).await {
Ok(user) => Ok(Json(user)),
Err(err) => Err(ApiError::Internal(err.into())),
}
}
Обработка паники
По умолчанию, если хендлер паникует, Axum возвращает ответ 500 Internal Server Error. Вы можете добавить обработчик паники, чтобы лучше контролировать этот процесс:
use tower::ServiceBuilder;
use tower_http::catch_panic::CatchPanicLayer;
let app = Router::new()
.route("/", get(handler))
// Добавление слоя для обработки паники
.layer(
ServiceBuilder::new()
.layer(CatchPanicLayer::new())
);
Композиция хендлеров
Хендлеры можно компоновать и комбинировать различными способами.
Хендлеры высшего порядка (middleware-подобные функции)
Вы можете создавать функции, которые принимают хендлер и возвращают новый хендлер с дополнительной функциональностью:
use std::time::Instant;
use axum::{
response::Response,
handler::Handler,
};
// Функция для замера времени выполнения хендлера
fn with_timing<H>(handler: H) -> impl Handler<H::Output> + Copy
where
H: Handler + Copy,
{
move |req| async move {
let start = Instant::now();
// Вызов оригинального хендлера
let response = handler.call(req).await;
let duration = start.elapsed();
tracing::info!("Обработка заняла {:?}", duration);
response
}
}
// Использование
let app = Router::new()
.route("/", get(with_timing(hello_world)));
Последовательная обработка
Вы можете объединять хендлеры в цепочку, где выход одного становится входом для другого:
async fn validate_input(
Json(payload): Json<CreateUser>,
) -> Result<CreateUser, ApiError> {
if payload.name.is_empty() {
return Err(ApiError::BadRequest("Имя не может быть пустым".into()));
}
Ok(payload)
}
async fn process_user(
payload: CreateUser,
) -> Result<Json<User>, ApiError> {
// Обработка пользователя
let user = create_user_in_db(&payload).await?;
Ok(Json(user))
}
// Композиция хендлеров через middleware
let app = Router::new()
.route("/users",
post(validate_input.and_then(process_user))
);
Структурированные хендлеры
Для сложных приложений может быть удобно организовать хендлеры в виде методов структуры:
struct UserHandler {
db_pool: PgPool,
}
impl UserHandler {
pub fn new(db_pool: PgPool) -> Self {
Self { db_pool }
}
pub async fn list(&self) -> Result<Json<Vec<User>>, ApiError> {
let users = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(&self.db_pool)
.await
.map_err(|e| ApiError::Internal(e.into()))?;
Ok(Json(users))
}
pub async fn get(&self, Path(id): Path<i64>) -> Result<Json<User>, ApiError> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&self.db_pool)
.await
.map_err(|e| ApiError::Internal(e.into()))?;
user.map(Json)
.ok_or(ApiError::NotFound)
}
pub async fn create(&self, Json(payload): Json<CreateUser>) -> Result<Json<User>, ApiError> {
// Логика создания пользователя
let user = sqlx::query_as::<_, User>(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
)
.bind(&payload.name)
.bind(&payload.email)
.fetch_one(&self.db_pool)
.await
.map_err(|e| ApiError::Internal(e.into()))?;
Ok(Json(user))
}
}
// Регистрация хендлеров
fn create_app(db_pool: PgPool) -> Router {
let user_handler = UserHandler::new(db_pool);
Router::new()
.route("/users", get(|h: State<Arc<UserHandler>>| h.list()).post(|h, payload| h.create(payload)))
.route("/users/:id", get(|h, id| h.get(id)))
.with_state(Arc::new(user_handler))
}
Тестирование хендлеров
Axum делает тестирование хендлеров простым и удобным:
#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;
#[tokio::test]
async fn test_hello_world() {
// Создание тестового приложения
let app = Router::new()
.route("/", get(hello_world));
// Создание тестового запроса
let request = Request::builder()
.uri("/")
.body(Body::empty())
.unwrap();
// Получение ответа
let response = app
.oneshot(request)
.await
.unwrap();
// Проверка статус-кода
assert_eq!(response.status(), StatusCode::OK);
// Проверка тела ответа
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"Hello, world!");
}
#[tokio::test]
async fn test_create_user() {
// Создание тестового приложения
let app = Router::new()
.route("/users", post(create_user));
// Создание тестового запроса с JSON-телом
let request = Request::builder()
.uri("/users")
.method("POST")
.header("Content-Type", "application/json")
.body(Body::from(r#"{"name":"Тест"}"#))
.unwrap();
// Получение ответа
let response = app
.oneshot(request)
.await
.unwrap();
// Проверка статус-кода
assert_eq!(response.status(), StatusCode::CREATED);
// Проверка тела ответа
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let user: User = serde_json::from_slice(&body).unwrap();
assert_eq!(user.name, "Тест");
}
}
Лучшие практики для хендлеров
Структурирование кода
- Разделяйте хендлеры по функциональности: создавайте отдельные модули для связанных групп хендлеров
- Избегайте монолитных хендлеров: разделяйте большие хендлеры на меньшие, более специализированные функции
- Отделяйте бизнес-логику от обработки HTTP: хендлеры должны преобразовывать HTTP-запросы в вызовы сервисных функций
// Хендлер получает данные из запроса и делегирует бизнес-логику сервису
async fn create_user(
State(service): State<UserService>,
Json(payload): Json<CreateUser>,
) -> Result<Json<User>, ApiError> {
// Хендлер отвечает только за HTTP-аспекты (парсинг, валидация, формат ответа)
let user = service.create_user(payload).await?;
Ok(Json(user))
}
Обработка ошибок
- Используйте типизированные ошибки вместо общих статус-кодов
- Предоставляйте осмысленные сообщения об ошибках
- Не раскрывайте чувствительную информацию в сообщениях об ошибках
- Логируйте детали ошибок для отладки
Производительность
- Избегайте блокирующих операций в асинхронных хендлерах
- Используйте
spawn_blocking
для CPU-интенсивных задач - Применяйте кэширование для часто запрашиваемых данных
- Будьте осторожны с захватом переменных в хендлерах-замыканиях
Безопасность
- Валидируйте все входные данные перед обработкой
- Используйте типобезопасные параметры пути вместо строковых
- Применяйте принцип наименьших привилегий — хендлеры должны иметь доступ только к необходимым ресурсам
Заключение
Хендлеры — это ядро любого веб-приложения на Axum. Их гибкость и выразительность позволяют создавать чистый, поддерживаемый и эффективный код. Умелое использование хендлеров, экстракторов и механизмов ответов делает разработку веб-сервисов на Rust удобной и продуктивной.
В следующем разделе мы рассмотрим работу с запросами и ответами в Axum более подробно.