Skip to content

Хендлеры

Хендлеры (обработчики) — это основные строительные блоки веб-приложений на Axum. Они отвечают за обработку входящих HTTP-запросов и формирование ответов. В этом разделе мы подробно рассмотрим, как создавать и использовать хендлеры, а также познакомимся с их возможностями и особенностями.

Что такое хендлер?

В Axum хендлер — это функция или тип, который реализует типаж (trait) Handler. Хендлеры принимают данные из запроса через параметры (экстракторы) и возвращают результаты, которые могут быть преобразованы в HTTP-ответы.

rust
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 обычно имеют следующую структуру:

rust
async fn handler_name(
    // Параметры-экстракторы (0 или более)
) -> тип_возвращаемого_значения {
    // Логика обработки
}

Почти всегда хендлеры должны быть асинхронными функциями (с ключевым словом async), но есть исключения, которые мы рассмотрим позже.

Возвращаемые типы

Хендлеры могут возвращать различные типы, которые реализуют типаж IntoResponse. Вот наиболее распространенные варианты:

rust
// Строка - превращается в текстовый ответ с 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 для собственных типов:

rust
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-запроса.

rust
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. Это позволяет эффективно обрабатывать множество запросов одновременно без блокирования потоков.

rust
async fn async_handler() -> &'static str {
    // Выполнение асинхронных операций
    tokio::time::sleep(Duration::from_millis(100)).await;
    "Готово!"
}

Выполнение блокирующих операций

Иногда вам может потребоваться выполнить операцию, которая блокирует поток выполнения (например, синхронный ввод-вывод или вычисления). В таких случаях используйте spawn_blocking из Tokio:

rust
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
}

Хендлеры-замыкания

Помимо именованных функций, вы можете использовать анонимные функции и замыкания как хендлеры:

rust
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 также поддерживает синхронные хендлеры для простых случаев:

rust
// Синхронный хендлер (без async)
fn sync_handler() -> &'static str {
    "Я синхронный хендлер!"
}

let app = Router::new()
    .route("/sync", get(sync_handler));

Синхронные хендлеры автоматически оборачиваются в асинхронный контекст, поэтому снаружи они ведут себя так же, как асинхронные.

Обработка ошибок

Axum предоставляет несколько подходов к обработке ошибок в хендлерах.

Использование Result

Самый простой способ — возвращать Result<T, E>, где T и E реализуют IntoResponse:

rust
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),
    }
}

Кастомные типы ошибок

Для более сложных случаев можно создать собственные типы ошибок:

rust
// Определение собственного типа ошибки
#[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. Вы можете добавить обработчик паники, чтобы лучше контролировать этот процесс:

rust
use tower::ServiceBuilder;
use tower_http::catch_panic::CatchPanicLayer;

let app = Router::new()
    .route("/", get(handler))
    // Добавление слоя для обработки паники
    .layer(
        ServiceBuilder::new()
            .layer(CatchPanicLayer::new())
    );

Композиция хендлеров

Хендлеры можно компоновать и комбинировать различными способами.

Хендлеры высшего порядка (middleware-подобные функции)

Вы можете создавать функции, которые принимают хендлер и возвращают новый хендлер с дополнительной функциональностью:

rust
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)));

Последовательная обработка

Вы можете объединять хендлеры в цепочку, где выход одного становится входом для другого:

rust
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))
    );

Структурированные хендлеры

Для сложных приложений может быть удобно организовать хендлеры в виде методов структуры:

rust
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 делает тестирование хендлеров простым и удобным:

rust
#[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-запросы в вызовы сервисных функций
rust
// Хендлер получает данные из запроса и делегирует бизнес-логику сервису
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 более подробно.