Skip to content

Extensions

Extensions в Axum — это мощный механизм для хранения и передачи произвольных данных через контекст запроса. В отличие от состояния приложения, которое является глобальным и создается при запуске сервера, extensions привязаны к конкретному запросу и могут быть изменены в процессе его обработки. Этот механизм основан на http::Extensions из крейта http и предоставляет гибкий способ передачи данных между различными частями обработчиков и middleware.

Основные концепции

Что такое Extensions

Extensions — это типизированное хранилище данных, связанное с HTTP-запросом. Оно позволяет:

  • Добавлять произвольные типы данных к запросу
  • Получать эти данные в обработчиках или middleware
  • Передавать информацию между middleware и обработчиками
  • Реализовать сквозные концепции, такие как контекст запроса или данные пользователя

Каждый тип может быть представлен в extensions только один раз, что обеспечивает уникальность и предсказуемость при извлечении данных.

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

Добавление данных в Extensions

Добавлять данные в extensions можно несколькими способами:

Через middleware

Самый распространенный подход — использование middleware для добавления данных в extensions:

rust
use axum::{
    Router,
    routing::get,
    http::Request,
    middleware::{self, Next},
    response::Response,
};

// Определение типа, который будем хранить в extensions
#[derive(Debug, Clone)]
struct RequestId(String);

// Middleware для генерации и добавления RequestId
async fn request_id_middleware<B>(
    request: Request<B>,
    next: Next<B>,
) -> Response {
    // Создаем новый Request ID
    let request_id = RequestId(uuid::Uuid::new_v4().to_string());
    
    // Получаем мутабельный доступ к extensions
    let mut request = request;
    request.extensions_mut().insert(request_id.clone());
    
    // Добавляем ID в заголовок ответа
    let mut response = next.run(request).await;
    response.headers_mut().insert(
        "x-request-id",
        request_id.0.parse().unwrap(),
    );
    
    response
}

// Обработчик, использующий RequestId из extensions
async fn handler(
    Extension(request_id): Extension<RequestId>,
) -> String {
    format!("Request ID: {}", request_id.0)
}

// Добавление middleware к роутеру
let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(request_id_middleware));

Через функцию AddExtension

Tower HTTP предоставляет middleware AddExtensionLayer, который позволяет добавлять extensions декларативно:

rust
use axum::{Router, routing::get, Extension};
use tower_http::add_extension::AddExtensionLayer;

// Определение типа конфигурации
#[derive(Clone)]
struct AppConfig {
    api_url: String,
    timeout: std::time::Duration,
}

// Создание конфигурации
let config = AppConfig {
    api_url: "https://api.example.com".to_string(),
    timeout: std::time::Duration::from_secs(30),
};

// Добавление конфигурации как extension
let app = Router::new()
    .route("/", get(handler))
    .layer(AddExtensionLayer::new(config));

// Обработчик, получающий конфигурацию
async fn handler(
    Extension(config): Extension<AppConfig>,
) -> String {
    format!("API URL: {}, Timeout: {:?}", config.api_url, config.timeout)
}

Модификация Extensions внутри обработчика

Внутри обработчика или middleware вы можете модифицировать extensions запроса:

rust
use axum::{
    http::Request,
    middleware::{self, Next},
    response::Response,
};

async fn audit_middleware<B>(
    mut request: Request<B>,
    next: Next<B>,
) -> Response {
    // Добавляем время начала обработки
    let start_time = std::time::Instant::now();
    request.extensions_mut().insert(start_time);
    
    // Передаем запрос дальше
    let response = next.run(request).await;
    
    response
}

Извлечение данных из Extensions

Для извлечения данных из extensions в Axum используется экстрактор Extension:

rust
use axum::{Extension, Json};
use serde::Serialize;

#[derive(Clone)]
struct CurrentUser {
    id: u64,
    username: String,
    roles: Vec<String>,
}

#[derive(Serialize)]
struct UserResponse {
    id: u64,
    username: String,
}

async fn get_current_user(
    Extension(user): Extension<CurrentUser>,
) -> Json<UserResponse> {
    Json(UserResponse {
        id: user.id,
        username: user.username,
    })
}

Если запрашиваемый тип отсутствует в extensions, Axum вернет ошибку 500 Internal Server Error. Чтобы избежать этого, можно использовать опциональное извлечение:

rust
use axum::Extension;
use std::sync::Arc;

#[derive(Default, Clone)]
struct RequestContext {
    trace_id: Option<String>,
    user_id: Option<u64>,
}

async fn handler(
    // Используем Option для опциональных extensions
    Extension(context): Extension<Option<Arc<RequestContext>>>,
) -> String {
    // Если контекст отсутствует, используем значение по умолчанию
    let context = context.unwrap_or_default();
    
    format!(
        "Trace ID: {}, User ID: {}", 
        context.trace_id.as_deref().unwrap_or("none"),
        context.user_id.unwrap_or(0)
    )
}

Типичные сценарии использования Extensions

Аутентификация и контекст пользователя

Одно из самых распространенных применений extensions — хранение информации о пользователе после аутентификации:

rust
use axum::{
    Router,
    routing::get,
    http::{Request, StatusCode},
    middleware::{self, Next},
    response::{Response, IntoResponse},
    Extension,
    Json,
};
use serde::Serialize;
use std::sync::Arc;

#[derive(Clone)]
struct User {
    id: u64,
    username: String,
    is_admin: bool,
}

// Middleware для аутентификации
async fn auth_middleware<B>(
    mut request: Request<B>,
    next: Next<B>,
) -> Response {
    // Извлекаем токен из заголовка Authorization
    let auth_header = request.headers()
        .get("Authorization")
        .and_then(|header| header.to_str().ok())
        .and_then(|header| header.strip_prefix("Bearer "));
    
    // Проверяем токен и получаем пользователя
    if let Some(token) = auth_header {
        // В реальном приложении здесь была бы проверка токена
        if token == "valid_token" {
            // Создаем объект пользователя
            let user = User {
                id: 1,
                username: "admin".to_string(),
                is_admin: true,
            };
            
            // Добавляем пользователя в extensions
            request.extensions_mut().insert(Arc::new(user));
            
            return next.run(request).await;
        }
    }
    
    // Если токен отсутствует или неверный, возвращаем 401
    StatusCode::UNAUTHORIZED.into_response()
}

// Middleware для проверки прав администратора
async fn admin_only<B>(
    request: Request<B>,
    next: Next<B>,
) -> Response {
    // Извлекаем пользователя из extensions
    let user = request.extensions()
        .get::<Arc<User>>()
        .cloned();
    
    // Проверяем, является ли пользователь администратором
    match user {
        Some(user) if user.is_admin => next.run(request).await,
        _ => StatusCode::FORBIDDEN.into_response(),
    }
}

#[derive(Serialize)]
struct UserProfile {
    id: u64,
    username: String,
}

// Обработчик профиля пользователя
async fn profile(
    Extension(user): Extension<Arc<User>>,
) -> Json<UserProfile> {
    Json(UserProfile {
        id: user.id,
        username: user.username.clone(),
    })
}

// Обработчик, доступный только администраторам
async fn admin_panel() -> &'static str {
    "Welcome to Admin Panel"
}

// Настройка маршрутов
let app = Router::new()
    // Маршрут, требующий только аутентификации
    .route("/profile", get(profile))
    // Маршруты, требующие прав администратора
    .route("/admin", get(admin_panel))
    .layer(middleware::from_fn(admin_only))
    // Общий middleware аутентификации
    .layer(middleware::from_fn(auth_middleware));

Централизованная обработка ошибок

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

rust
use axum::{
    Router,
    routing::get,
    http::Request,
    middleware::{self, Next},
    response::{Response, IntoResponse},
    Extension,
    Json,
};
use serde::Serialize;
use std::sync::{Arc, Mutex};

// Структура для сбора ошибок и предупреждений
#[derive(Default, Clone)]
struct ErrorCollector {
    errors: Arc<Mutex<Vec<String>>>,
    warnings: Arc<Mutex<Vec<String>>>,
}

impl ErrorCollector {
    fn add_error(&self, error: impl Into<String>) {
        self.errors.lock().unwrap().push(error.into());
    }
    
    fn add_warning(&self, warning: impl Into<String>) {
        self.warnings.lock().unwrap().push(warning.into());
    }
    
    fn has_errors(&self) -> bool {
        !self.errors.lock().unwrap().is_empty()
    }
}

// Middleware для инициализации сборщика ошибок
async fn error_collector_middleware<B>(
    mut request: Request<B>,
    next: Next<B>,
) -> Response {
    let collector = ErrorCollector::default();
    request.extensions_mut().insert(collector.clone());
    
    let mut response = next.run(request).await;
    
    // Если есть ошибки, добавляем их в заголовки ответа
    if collector.has_errors() {
        response.headers_mut().insert(
            "X-Application-Errors",
            "true".parse().unwrap(),
        );
    }
    
    response
}

// Обработчик с использованием сборщика ошибок
async fn handler(
    Extension(collector): Extension<ErrorCollector>,
) -> impl IntoResponse {
    // Добавляем предупреждение
    collector.add_warning("This is a deprecated endpoint");
    
    // Пытаемся выполнить операцию
    let result = std::fs::read_to_string("/non/existent/file");
    
    if let Err(err) = result {
        // Добавляем ошибку
        collector.add_error(format!("Failed to read file: {}", err));
        
        // Возвращаем информацию об ошибке клиенту
        return (
            StatusCode::INTERNAL_SERVER_ERROR,
            "Failed to process request".to_string(),
        ).into_response();
    }
    
    "Operation successful".into_response()
}

// Настройка маршрутов
let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(error_collector_middleware));

Контекст запроса

Extensions часто используются для создания контекста запроса, содержащего информацию, которая может потребоваться на разных этапах обработки:

rust
use axum::{
    Router,
    routing::get,
    http::Request,
    middleware::{self, Next},
    response::Response,
    Extension,
};
use std::sync::Arc;
use std::time::Instant;

// Структура контекста запроса
#[derive(Clone)]
struct RequestContext {
    request_id: String,
    start_time: Instant,
    client_ip: String,
    path: String,
    method: String,
}

// Middleware для создания и добавления контекста
async fn context_middleware<B>(
    request: Request<B>,
    next: Next<B>,
) -> Response {
    let request_id = uuid::Uuid::new_v4().to_string();
    let start_time = Instant::now();
    
    let client_ip = request
        .headers()
        .get("X-Forwarded-For")
        .and_then(|h| h.to_str().ok())
        .unwrap_or("unknown")
        .to_string();
    
    let path = request.uri().path().to_string();
    let method = request.method().to_string();
    
    let context = RequestContext {
        request_id,
        start_time,
        client_ip,
        path,
        method,
    };
    
    let mut request = request;
    request.extensions_mut().insert(Arc::new(context));
    
    let response = next.run(request).await;
    
    response
}

// Обработчик, использующий контекст
async fn handler(
    Extension(context): Extension<Arc<RequestContext>>,
) -> String {
    let duration = context.start_time.elapsed();
    
    format!(
        "Request ID: {}, Duration: {:?}, Client IP: {}, Path: {}, Method: {}",
        context.request_id,
        duration,
        context.client_ip,
        context.path,
        context.method
    )
}

// Настройка маршрутов
let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(context_middleware));

Лучшие практики работы с Extensions

Использование Arc для снижения накладных расходов

Для эффективного клонирования данных в extensions рекомендуется использовать Arc:

rust
use std::sync::Arc;

// Вместо этого
request.extensions_mut().insert(my_large_struct);

// Лучше использовать Arc
request.extensions_mut().insert(Arc::new(my_large_struct));

Типобезопасность с newtype паттерном

Для предотвращения коллизий и улучшения читаемости используйте newtype паттерн для ваших типов:

rust
// Вместо этого
request.extensions_mut().insert("request_id".to_string());

// Лучше создать новый тип
#[derive(Clone, Debug)]
struct RequestId(String);

request.extensions_mut().insert(RequestId("abc123".to_string()));

Функции-помощники для работы с Extensions

Создавайте удобные функции для работы с extensions:

rust
use axum::http::Request;

// Функция для добавления токена
fn insert_token<B>(request: &mut Request<B>, token: String) {
    request.extensions_mut().insert(AuthToken(token));
}

// Функция для извлечения токена
fn get_token<B>(request: &Request<B>) -> Option<String> {
    request.extensions()
        .get::<AuthToken>()
        .map(|token| token.0.clone())
}

Сочетание Extensions и State

Extensions хорошо сочетаются с состоянием приложения:

rust
use axum::{
    Router,
    routing::get,
    extract::State,
    Extension,
};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db_pool: PgPool,
    config: Config,
}

#[derive(Clone)]
struct RequestUser {
    id: u64,
    username: String,
}

async fn handler(
    State(state): State<AppState>,         // Глобальное состояние приложения
    Extension(user): Extension<RequestUser>, // Данные конкретного запроса
) -> String {
    format!("Hello, {}! DB connection: {}", user.username, state.db_pool.is_connected())
}

Обработка отсутствующих Extensions

Всегда проверяйте наличие extensions или используйте fallback:

rust
use axum::{
    http::Request,
    response::{Response, IntoResponse},
    Extension,
};

// Option для опциональных значений
async fn handler(
    Extension(user): Extension<Option<User>>,
) -> impl IntoResponse {
    if let Some(user) = user {
        format!("Hello, {}", user.username)
    } else {
        "Hello, guest".to_string()
    }
}

// Или используйте middleware для проверки
async fn require_user<B>(
    request: Request<B>,
    next: Next<B>,
) -> Response {
    if request.extensions().get::<User>().is_some() {
        next.run(request).await
    } else {
        (StatusCode::UNAUTHORIZED, "Authentication required").into_response()
    }
}

Ограничения и особенности

Uniqueness

В extensions может быть только одно значение каждого типа. Если вы добавите второе значение того же типа, оно заменит первое:

rust
request.extensions_mut().insert(Counter(1));
request.extensions_mut().insert(Counter(2)); // Заменит предыдущее значение

let counter = request.extensions().get::<Counter>().unwrap();
assert_eq!(counter.0, 2);

Производительность

Доступ к extensions требует выполнения динамической типизации (downcasting), что немного менее эффективно, чем прямой доступ к полям структуры. Однако в большинстве случаев это не является существенным фактором производительности.

Время жизни

Extensions существуют только в контексте запроса. Если вам нужно хранить данные между запросами, используйте состояние приложения (State).

Заключение

Extensions в Axum предоставляют гибкий и мощный способ передачи данных между различными частями обработки HTTP-запроса. Они особенно полезны для:

  • Передачи информации между middleware и обработчиками
  • Хранения контекста запроса и аутентификационных данных
  • Реализации сквозной функциональности, такой как логирование или трассировка

Правильное использование extensions делает код более модульным и легко тестируемым, позволяя отделить бизнес-логику от инфраструктурных задач.