Кастомные экстракторы в Axum
Экстракторы в Axum — это компоненты, которые извлекают данные из HTTP-запросов. В этом разделе мы рассмотрим, как создавать собственные (кастомные) экстракторы для специфических сценариев использования.
Содержание
- Основы экстракторов в Axum
- Создание простого экстрактора
- Создание асинхронного экстрактора
- Экстракторы с отказоустойчивостью
- Экстракторы с доступом к состоянию
- Экстракторы с валидацией
- Примеры полезных экстракторов
- Лучшие практики
Основы экстракторов в Axum
В Axum экстракторы реализуются через трейт FromRequest
, который определяет, как извлекать данные из HTTP-запроса:
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts},
http::StatusCode,
};
#[async_trait]
impl<B> FromRequest<B> for MyExtractor
where
B: Send, // Требование для тела запроса
{
type Rejection = (StatusCode, String); // Тип, возвращаемый при ошибке
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Логика извлечения данных
Ok(MyExtractor { /* ... */ })
}
}
Создание простого экстрактора
Рассмотрим пример создания простого экстрактора, который извлекает пользовательский агент из заголовков:
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts},
http::StatusCode,
};
// Определение экстрактора
pub struct UserAgent(pub String);
#[async_trait]
impl<B> FromRequest<B> for UserAgent
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение заголовка User-Agent
let user_agent = req
.headers()
.get(axum::http::header::USER_AGENT)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string());
match user_agent {
Some(agent) => Ok(UserAgent(agent)),
None => Err((
StatusCode::BAD_REQUEST,
"Заголовок User-Agent отсутствует".to_string(),
)),
}
}
}
// Использование экстрактора в обработчике
async fn handler(UserAgent(user_agent): UserAgent) -> String {
format!("Ваш User-Agent: {}", user_agent)
}
Создание асинхронного экстрактора
Для более сложных сценариев, когда извлечение данных требует асинхронных операций (например, запросы к базе данных), можно создать асинхронный экстрактор:
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts, State},
http::StatusCode,
};
use sqlx::PgPool;
use uuid::Uuid;
// Модель пользователя
#[derive(Debug)]
pub struct AuthUser {
pub id: Uuid,
pub username: String,
pub role: String,
}
#[async_trait]
impl<B> FromRequest<B> for AuthUser
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение токена из заголовка Authorization
let auth_header = req
.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok());
let token = match auth_header {
Some(value) if value.starts_with("Bearer ") => value[7..].to_string(),
_ => return Err((
StatusCode::UNAUTHORIZED,
"Отсутствует или некорректный токен авторизации".to_string(),
)),
};
// Получение состояния приложения с подключением к БД
let state = State::<PgPool>::from_request(req)
.await
.map_err(|_| (
StatusCode::INTERNAL_SERVER_ERROR,
"Ошибка подключения к базе данных".to_string(),
))?;
// Поиск пользователя по токену (асинхронный запрос к БД)
let user = sqlx::query_as!(
AuthUser,
"SELECT id, username, role FROM users WHERE auth_token = $1",
token
)
.fetch_optional(&*state)
.await
.map_err(|_| (
StatusCode::INTERNAL_SERVER_ERROR,
"Ошибка базы данных".to_string(),
))?;
match user {
Some(user) => Ok(user),
None => Err((
StatusCode::UNAUTHORIZED,
"Недействительный токен".to_string(),
)),
}
}
}
// Использование в обработчике
async fn protected_handler(user: AuthUser) -> String {
format!("Привет, {}! Ваша роль: {}", user.username, user.role)
}
Экстракторы с отказоустойчивостью
Иногда полезно иметь экстрактор, который не прерывает запрос при неудаче, а возвращает Option
или значение по умолчанию:
use async_trait::async_trait;
use axum::extract::{FromRequest, RequestParts};
// Экстрактор, который может отсутствовать
pub struct OptionalUserAgent(pub Option<String>);
#[async_trait]
impl<B> FromRequest<B> for OptionalUserAgent
where
B: Send,
{
type Rejection = (); // Этот экстрактор никогда не отклоняет запрос
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Попытка получить заголовок User-Agent
let user_agent = req
.headers()
.get(axum::http::header::USER_AGENT)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string());
Ok(OptionalUserAgent(user_agent))
}
}
// Использование в обработчике
async fn optional_agent_handler(
OptionalUserAgent(user_agent): OptionalUserAgent,
) -> String {
match user_agent {
Some(agent) => format!("Ваш User-Agent: {}", agent),
None => "User-Agent не предоставлен".to_string(),
}
}
Экстракторы с доступом к состоянию
Создание экстрактора, который использует состояние приложения:
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts, State},
http::StatusCode,
};
use std::sync::{Arc, RwLock};
// Структура конфигурации
#[derive(Clone)]
pub struct AppConfig {
pub debug_mode: bool,
pub api_version: String,
}
// Экстрактор для конфигурации
pub struct ExtractConfig(pub AppConfig);
#[async_trait]
impl<B> FromRequest<B> for ExtractConfig
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение состояния приложения
let state = State::<Arc<RwLock<AppConfig>>>::from_request(req)
.await
.map_err(|_| (
StatusCode::INTERNAL_SERVER_ERROR,
"Не удалось получить конфигурацию".to_string(),
))?;
// Клонирование конфигурации из состояния
let config = state.read()
.map_err(|_| (
StatusCode::INTERNAL_SERVER_ERROR,
"Ошибка блокировки".to_string(),
))?
.clone();
Ok(ExtractConfig(config))
}
}
// Использование в обработчике
async fn config_handler(
ExtractConfig(config): ExtractConfig,
) -> String {
format!(
"Режим отладки: {}, Версия API: {}",
config.debug_mode,
config.api_version
)
}
Экстракторы с валидацией
Создание экстрактора, который валидирует данные при извлечении:
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts, Json},
http::StatusCode,
};
use serde::Deserialize;
use validator::Validate;
// Структура данных с валидацией
#[derive(Debug, Deserialize, Validate)]
pub struct CreateUser {
#[validate(length(min = 3, max = 50, message = "Username must be between 3 and 50 characters"))]
pub username: String,
#[validate(email(message = "Invalid email format"))]
pub email: String,
#[validate(length(min = 8, message = "Password must be at least 8 characters"))]
pub password: String,
}
// Экстрактор с валидацией
pub struct ValidatedJson<T>(pub T);
#[async_trait]
impl<B, T> FromRequest<B> for ValidatedJson<T>
where
B: Send,
T: Validate + Send + 'static,
Json<T>: FromRequest<B, Rejection = axum::extract::rejection::JsonRejection>,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Извлечение JSON из запроса
let Json(data) = Json::<T>::from_request(req)
.await
.map_err(|err| {
let message = format!("Ошибка JSON: {}", err);
(StatusCode::BAD_REQUEST, message)
})?;
// Валидация данных
data.validate().map_err(|err| {
(StatusCode::UNPROCESSABLE_ENTITY, format!("Ошибка валидации: {}", err))
})?;
Ok(ValidatedJson(data))
}
}
// Использование в обработчике
async fn create_user_handler(
ValidatedJson(user): ValidatedJson<CreateUser>,
) -> String {
format!(
"Пользователь создан: {} ({})",
user.username,
user.email
)
}
Примеры полезных экстракторов
1. Экстрактор для пагинации
use async_trait::async_trait;
use axum::{
extract::{FromRequest, Query, RequestParts},
http::StatusCode,
};
use serde::Deserialize;
// Параметры пагинации
#[derive(Deserialize)]
struct PaginationParams {
page: Option<usize>,
per_page: Option<usize>,
}
// Экстрактор пагинации с ограничениями
pub struct Pagination {
pub page: usize,
pub per_page: usize,
pub offset: usize,
}
#[async_trait]
impl<B> FromRequest<B> for Pagination
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Извлечение query-параметров
let Query(params) = Query::<PaginationParams>::from_request(req)
.await
.map_err(|_| {
(StatusCode::BAD_REQUEST, "Некорректные параметры пагинации".to_string())
})?;
// Установка значений по умолчанию и применение ограничений
let page = params.page.unwrap_or(1).max(1);
let per_page = params.per_page.unwrap_or(20).clamp(1, 100);
let offset = (page - 1) * per_page;
Ok(Pagination {
page,
per_page,
offset,
})
}
}
// Использование экстрактора
async fn list_users(pagination: Pagination) -> String {
format!(
"Список пользователей: страница {}, по {} элементов (смещение: {})",
pagination.page,
pagination.per_page,
pagination.offset
)
}
2. Экстрактор для API-ключа
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts, State},
http::{header, StatusCode},
};
// Структура для API-ключа
pub struct ApiKey(pub String);
#[async_trait]
impl<B> FromRequest<B> for ApiKey
where
B: Send,
{
type Rejection = (StatusCode, &'static str);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Сначала проверяем заголовок X-API-Key
if let Some(api_key) = req
.headers()
.get("X-API-Key")
.and_then(|value| value.to_str().ok())
{
return Ok(ApiKey(api_key.to_string()));
}
// Затем проверяем query-параметр api_key
if let Some(query) = req.uri().query() {
if let Some(key_pos) = query.find("api_key=") {
let key_start = key_pos + 8; // длина "api_key="
let key_end = query[key_start..]
.find('&')
.map(|pos| key_start + pos)
.unwrap_or(query.len());
let api_key = &query[key_start..key_end];
if !api_key.is_empty() {
return Ok(ApiKey(api_key.to_string()));
}
}
}
// API-ключ не найден
Err((StatusCode::UNAUTHORIZED, "API-ключ отсутствует или недействителен"))
}
}
// Использование экстрактора
async fn api_endpoint(ApiKey(key): ApiKey) -> String {
format!("Запрос с API-ключом: {}", key)
}
3. Экстрактор для геолокации по IP
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts, ConnectInfo},
http::StatusCode,
};
use std::net::SocketAddr;
use serde::Serialize;
// Структура для информации о геолокации
#[derive(Debug, Serialize)]
pub struct GeoLocation {
pub country: String,
pub city: String,
pub latitude: f64,
pub longitude: f64,
}
#[async_trait]
impl<B> FromRequest<B> for GeoLocation
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение IP-адреса клиента
let connect_info = ConnectInfo::<SocketAddr>::from_request(req)
.await
.map_err(|_| (
StatusCode::INTERNAL_SERVER_ERROR,
"Не удалось получить информацию о подключении".to_string(),
))?;
let ip = connect_info.ip();
// В реальном приложении здесь был бы запрос к сервису геолокации
// Для примера возвращаем фиктивные данные
let geo = match ip.to_string().as_str() {
"127.0.0.1" => GeoLocation {
country: "Local".to_string(),
city: "Localhost".to_string(),
latitude: 0.0,
longitude: 0.0,
},
_ => GeoLocation {
country: "Russia".to_string(),
city: "Moscow".to_string(),
latitude: 55.7558,
longitude: 37.6173,
},
};
Ok(geo)
}
}
// Использование экстрактора
async fn geo_handler(geo: GeoLocation) -> String {
format!(
"Ваше местоположение: {}, {} (координаты: {}, {})",
geo.city,
geo.country,
geo.latitude,
geo.longitude
)
}
Лучшие практики
1. Используйте информативные сообщения об ошибках
// Плохо
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
match get_token(req) {
Some(token) => Ok(MyExtractor { token }),
None => Err((StatusCode::UNAUTHORIZED, "Unauthorized")),
}
}
// Хорошо
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
match get_token(req) {
Some(token) => Ok(MyExtractor { token }),
None => Err((
StatusCode::UNAUTHORIZED,
"Заголовок Authorization отсутствует или имеет неверный формат".to_string(),
)),
}
}
2. Разделяйте сложные экстракторы на более простые
// Плохо (один большой экстрактор)
pub struct UserData {
pub user_id: Uuid,
pub token: String,
pub permissions: Vec<String>,
}
// Хорошо (композиция экстракторов)
pub struct AuthToken(pub String);
pub struct UserInfo { pub id: Uuid, pub name: String }
pub struct UserPermissions(pub Vec<String>);
async fn handler(
AuthToken(token): AuthToken,
user: UserInfo,
UserPermissions(permissions): UserPermissions,
) -> impl IntoResponse {
// Обработка с использованием всех извлеченных данных
}
3. Используйте дженерики для повторно используемых экстракторов
use async_trait::async_trait;
use axum::extract::{FromRequest, RequestParts};
use serde::de::DeserializeOwned;
use validator::Validate;
// Дженерик-экстрактор для валидируемых данных
pub struct Validated<T>(pub T);
#[async_trait]
impl<B, T> FromRequest<B> for Validated<T>
where
B: Send,
T: DeserializeOwned + Validate + Send + 'static,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Реализация...
}
}
// Использование с разными типами
async fn create_user(Validated(user): Validated<CreateUser>) -> impl IntoResponse {
// ...
}
async fn create_post(Validated(post): Validated<CreatePost>) -> impl IntoResponse {
// ...
}
4. Кэшируйте результаты для тяжелых экстракторов
use async_trait::async_trait;
use axum::{
extract::{FromRequest, RequestParts, FromRequestParts},
http::StatusCode,
Extension,
};
use std::sync::Arc;
// Тяжелый экстрактор с кэшированием
pub struct HeavyExtractor {
pub data: Arc<Vec<String>>,
}
#[async_trait]
impl<B> FromRequest<B> for HeavyExtractor
where
B: Send,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Проверяем, есть ли уже результат в расширениях
if let Ok(Extension(cached)) = Extension::<HeavyExtractor>::from_request(req).await {
return Ok(cached);
}
// Если нет, выполняем тяжелую операцию
let data = perform_heavy_operation().await?;
let extractor = HeavyExtractor {
data: Arc::new(data),
};
// Сохраняем результат в расширениях для последующих экстракторов
req.extensions_mut().insert(extractor.clone());
Ok(extractor)
}
}
// Вспомогательная функция
async fn perform_heavy_operation() -> Result<Vec<String>, (StatusCode, String)> {
// Имитация тяжелой асинхронной операции
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
Ok(vec!["data1".to_string(), "data2".to_string()])
}
5. Используйте трейты для абстракции общего поведения
use async_trait::async_trait;
// Трейт для объектов, которые можно аутентифицировать
#[async_trait]
pub trait Authenticable {
async fn authenticate(&self, token: &str) -> Result<(), String>;
fn get_user_id(&self) -> Uuid;
fn get_permissions(&self) -> Vec<String>;
}
// Экстрактор, использующий трейт
pub struct AuthenticatedUser<T: Authenticable>(pub T);
#[async_trait]
impl<B, T> FromRequest<B> for AuthenticatedUser<T>
where
B: Send,
T: Authenticable + Send + 'static,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Логика аутентификации с использованием трейта
}
}
Создание кастомных экстракторов в Axum позволяет значительно улучшить читаемость и структуру кода, вынося общую логику извлечения и валидации данных из обработчиков. Используйте эти паттерны для создания чистых, поддерживаемых веб-приложений на Axum.