Extensions
Extensions в Axum — это мощный механизм для хранения и передачи произвольных данных через контекст запроса. В отличие от состояния приложения, которое является глобальным и создается при запуске сервера, extensions привязаны к конкретному запросу и могут быть изменены в процессе его обработки. Этот механизм основан на http::Extensions
из крейта http
и предоставляет гибкий способ передачи данных между различными частями обработчиков и middleware.
Основные концепции
Что такое Extensions
Extensions — это типизированное хранилище данных, связанное с HTTP-запросом. Оно позволяет:
- Добавлять произвольные типы данных к запросу
- Получать эти данные в обработчиках или middleware
- Передавать информацию между middleware и обработчиками
- Реализовать сквозные концепции, такие как контекст запроса или данные пользователя
Каждый тип может быть представлен в extensions только один раз, что обеспечивает уникальность и предсказуемость при извлечении данных.
Использование Extensions в Axum
Добавление данных в Extensions
Добавлять данные в extensions можно несколькими способами:
Через middleware
Самый распространенный подход — использование middleware для добавления данных в extensions:
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 декларативно:
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 запроса:
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
:
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. Чтобы избежать этого, можно использовать опциональное извлечение:
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 — хранение информации о пользователе после аутентификации:
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 могут использоваться для сбора ошибок и предупреждений во время обработки запроса:
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 часто используются для создания контекста запроса, содержащего информацию, которая может потребоваться на разных этапах обработки:
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
:
use std::sync::Arc;
// Вместо этого
request.extensions_mut().insert(my_large_struct);
// Лучше использовать Arc
request.extensions_mut().insert(Arc::new(my_large_struct));
Типобезопасность с newtype паттерном
Для предотвращения коллизий и улучшения читаемости используйте newtype паттерн для ваших типов:
// Вместо этого
request.extensions_mut().insert("request_id".to_string());
// Лучше создать новый тип
#[derive(Clone, Debug)]
struct RequestId(String);
request.extensions_mut().insert(RequestId("abc123".to_string()));
Функции-помощники для работы с Extensions
Создавайте удобные функции для работы с extensions:
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 хорошо сочетаются с состоянием приложения:
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:
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 может быть только одно значение каждого типа. Если вы добавите второе значение того же типа, оно заменит первое:
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 делает код более модульным и легко тестируемым, позволяя отделить бизнес-логику от инфраструктурных задач.