Extractors (Экстракторы)
Экстракторы — это один из ключевых механизмов Axum, который позволяет извлекать данные из HTTP-запросов и предоставлять их хендлерам в типизированном виде. В этом разделе мы рассмотрим концепцию экстракторов, их типы и способы использования, а также научимся создавать собственные экстракторы для нестандартных задач.
Концепция экстракторов
Экстрактор в Axum — это тип, реализующий специальный трейт FromRequest
. Его основная задача — извлечь и преобразовать данные из HTTP-запроса в типизированное значение, которое затем будет передано хендлеру. Экстракторы значительно упрощают код хендлеров, избавляя от необходимости вручную извлекать и парсить данные из разных частей запроса.
// Без экстракторов код выглядел бы примерно так:
async fn create_user_manual(request: Request) -> Response {
// Извлечение и парсинг JSON из тела запроса
let body_bytes = hyper::body::to_bytes(request.into_body()).await.unwrap();
let user: CreateUser = serde_json::from_slice(&body_bytes).unwrap();
// Далее логика обработки...
}
// С экстракторами код становится более декларативным:
async fn create_user(
Json(user): Json<CreateUser>,
) -> impl IntoResponse {
// Сразу получаем типизированные данные
// Логика обработки...
}
Встроенные экстракторы
Axum предоставляет множество встроенных экстракторов для различных случаев использования.
1. Экстракторы пути (Path)
Экстрактор Path
извлекает параметры из URL-пути запроса:
use axum::{
extract::Path,
routing::get,
Router,
};
use serde::Deserialize;
// Извлечение одного параметра
async fn get_user(Path(user_id): Path<u64>) -> String {
format!("Получен пользователь с ID: {}", user_id)
}
// Извлечение нескольких параметров с использованием кортежа
async fn get_post_comment(
Path((post_id, comment_id)): Path<(u64, u64)>
) -> String {
format!("Получен комментарий {} для поста {}", comment_id, post_id)
}
// Извлечение параметров в структуру
#[derive(Deserialize)]
struct UserParams {
user_id: u64,
action: String,
}
async fn user_action(Path(params): Path<UserParams>) -> String {
format!("Действие {} для пользователя {}", params.action, params.user_id)
}
// Регистрация маршрутов
let app = Router::new()
.route("/users/:user_id", get(get_user))
.route("/posts/:post_id/comments/:comment_id", get(get_post_comment))
.route("/users/:user_id/:action", get(user_action));
2. Экстракторы Query-параметров (Query)
Экстрактор Query
извлекает параметры из строки запроса (query string):
use axum::{
extract::Query,
routing::get,
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
sort: Option<String>,
direction: Option<String>,
}
async fn list_users(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10);
let sort = params.sort.unwrap_or_else(|| "created_at".to_string());
let direction = params.direction.unwrap_or_else(|| "desc".to_string());
format!(
"Список пользователей: страница {}, элементов {}, сортировка {} {}",
page, per_page, sort, direction
)
}
// Можно десериализовывать в HashMap для произвольных параметров
async fn dynamic_params(
Query(params): Query<std::collections::HashMap<String, String>>
) -> String {
format!("Получены параметры: {:?}", params)
}
let app = Router::new()
.route("/users", get(list_users))
.route("/search", get(dynamic_params));
3. Экстракторы тела запроса (Json, Form)
Экстракторы Json
и Form
извлекают и десериализуют тело запроса:
use axum::{
extract::{Json, Form},
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
age: Option<u8>,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
age: Option<u8>,
}
// Обработка JSON-данных из тела запроса
async fn create_user_json(
Json(payload): Json<CreateUser>
) -> Json<User> {
// Создание пользователя...
Json(User {
id: 1,
name: payload.name,
email: payload.email,
age: payload.age,
})
}
// Обработка данных формы
async fn create_user_form(
Form(payload): Form<CreateUser>
) -> Json<User> {
// Создание пользователя...
Json(User {
id: 1,
name: payload.name,
email: payload.email,
age: payload.age,
})
}
let app = Router::new()
.route("/users/json", post(create_user_json))
.route("/users/form", post(create_user_form));
4. Экстрактор состояния (State)
Экстрактор State
предоставляет доступ к общему состоянию приложения:
use axum::{
extract::State,
routing::{get, post},
Router,
};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
// Тип состояния приложения
struct AppState {
db_pool: PgPool, // Подключение к базе данных
config: AppConfig, // Конфигурация приложения
user_counter: Mutex<u64>, // Счетчик пользователей
}
// Использование состояния в хендлере
async fn get_users(
State(state): State<Arc<AppState>>
) -> String {
let users = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(&state.db_pool)
.await
.unwrap();
format!("Найдено {} пользователей", users.len())
}
// Изменение состояния
async fn increment_counter(
State(state): State<Arc<AppState>>
) -> String {
let mut counter = state.user_counter.lock().unwrap();
*counter += 1;
format!("Текущее значение счетчика: {}", *counter)
}
// Инициализация состояния и приложения
let state = Arc::new(AppState {
db_pool: create_pg_pool().await,
config: load_config(),
user_counter: Mutex::new(0),
});
let app = Router::new()
.route("/users", get(get_users))
.route("/increment", post(increment_counter))
.with_state(state);
5. Экстракторы HTTP-примитивов (Method, HeaderMap, Uri)
Axum также позволяет извлекать различные HTTP-примитивы:
use axum::{
routing::get,
Router,
http::{Method, Uri, HeaderMap, Version},
};
// Извлечение HTTP-метода
async fn method_handler(method: Method) -> String {
format!("HTTP-метод: {}", method)
}
// Извлечение заголовков
async fn headers_handler(headers: HeaderMap) -> String {
let user_agent = headers.get("user-agent")
.map(|v| v.to_str().unwrap_or_default())
.unwrap_or_default();
format!("User-Agent: {}", user_agent)
}
// Извлечение URI
async fn uri_handler(uri: Uri) -> String {
format!("URI: {}", uri)
}
// Извлечение версии HTTP
async fn version_handler(version: Version) -> String {
format!("HTTP версия: {:?}", version)
}
let app = Router::new()
.route("/method", get(method_handler))
.route("/headers", get(headers_handler))
.route("/uri", get(uri_handler))
.route("/version", get(version_handler));
6. Экстрактор для запросов WebSocket
Axum предоставляет специальный экстрактор для обработки WebSocket-подключений:
use axum::{
extract::ws::{WebSocket, WebSocketUpgrade},
routing::get,
response::IntoResponse,
Router,
};
async fn websocket_handler(
ws: WebSocketUpgrade,
) -> impl IntoResponse {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(socket: WebSocket) {
let (mut sender, mut receiver) = socket.split();
// Пример эхо-сервера
while let Some(msg) = receiver.next().await {
if let Ok(msg) = msg {
if sender.send(msg).await.is_err() {
break;
}
} else {
break;
}
}
}
let app = Router::new()
.route("/ws", get(websocket_handler));
7. Экстрактор для загрузки файлов (Multipart)
Для работы с загрузкой файлов используется экстрактор Multipart
:
use axum::{
extract::Multipart,
routing::post,
Router,
};
use tokio::{fs::File, io::AsyncWriteExt};
async fn upload_handler(mut multipart: Multipart) -> String {
let mut uploaded_files = 0;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();
// Сохранение файла
let path = format!("./uploads/{}", file_name);
let mut file = File::create(&path).await.unwrap();
file.write_all(&data).await.unwrap();
uploaded_files += 1;
}
format!("Загружено {} файлов", uploaded_files)
}
let app = Router::new()
.route("/upload", post(upload_handler));
Комбинирование экстракторов
Одно из ключевых преимуществ системы экстракторов Axum — возможность комбинировать несколько экстракторов в одном хендлере:
use axum::{
extract::{Path, Query, Json, State},
routing::{get, post},
http::HeaderMap,
Router,
};
use serde::{Deserialize, Serialize};
// Хендлер, использующий несколько экстракторов
async fn complex_handler(
State(state): State<AppState>,
Path(user_id): Path<u64>,
Query(params): Query<FilterParams>,
headers: HeaderMap,
Json(payload): Json<UpdateUser>,
) -> impl IntoResponse {
// Доступ к всем извлеченным данным
// ...
}
let app = Router::new()
.route("/users/:user_id", post(complex_handler));
При использовании нескольких экстракторов важно учитывать, что:
- Экстракторы применяются в порядке параметров функции
- Если один из экстракторов возвращает ошибку, обработка запроса прерывается
- Некоторые экстракторы потребляют тело запроса (Json, Form, Multipart), поэтому можно использовать только один такой экстрактор
Обработка ошибок экстракции
Когда экстрактор не может извлечь данные из запроса (например, из-за неверного формата JSON), он возвращает ошибку, которая преобразуется в HTTP-ответ с соответствующим статус-кодом:
use axum::{
extract::Json,
routing::post,
http::StatusCode,
response::IntoResponse,
Router,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
// При неверном формате JSON Axum автоматически вернет ошибку 400 Bad Request
async fn create_user(Json(user): Json<CreateUser>) -> impl IntoResponse {
// Обработка данных...
}
// Можно также обрабатывать ошибки вручную с помощью экстрактора rejection
async fn create_user_custom(
payload: Result<Json<CreateUser>, axum::extract::rejection::JsonRejection>,
) -> impl IntoResponse {
match payload {
Ok(Json(user)) => {
// Обработка валидных данных
(StatusCode::CREATED, "Пользователь создан")
},
Err(err) => {
// Кастомная обработка ошибки
let error_message = format!("Ошибка JSON: {}", err);
tracing::error!(error_message);
let payload = json!({
"error": "invalid_json",
"message": error_message,
});
(StatusCode::BAD_REQUEST, axum::Json(payload))
}
}
}
let app = Router::new()
.route("/users", post(create_user))
.route("/users/custom", post(create_user_custom));
Опциональные экстракторы
Иногда требуется, чтобы экстрактор был опциональным. Axum поддерживает обертку экстракторов в Option
:
use axum::{
extract::{Query, Json},
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Filters {
sort: Option<String>,
limit: Option<u32>,
}
// Опциональные query-параметры
async fn list_items(
query: Option<Query<Filters>>,
) -> String {
let filters = query.unwrap_or_else(|| Query(Filters {
sort: None,
limit: None,
}));
let sort = filters.0.sort.unwrap_or_else(|| "id".to_string());
let limit = filters.0.limit.unwrap_or(10);
format!("Список элементов: сортировка {}, лимит {}", sort, limit)
}
// Опциональное тело запроса
async fn optional_body(
json: Option<Json<CreateUser>>,
) -> String {
match json {
Some(Json(user)) => format!("Получены данные: {}", user.name),
None => "Тело запроса отсутствует".to_string(),
}
}
let app = Router::new()
.route("/items", get(list_items))
.route("/optional", post(optional_body));
Создание собственных экстракторов
Одно из мощных свойств Axum — возможность создавать собственные экстракторы для нестандартных задач. Для этого нужно реализовать трейт FromRequest
:
use axum::{
async_trait,
extract::{FromRequest, RequestParts},
http::{StatusCode, header},
routing::get,
Router,
};
// Экстрактор для извлечения API-ключа из заголовков
struct ApiKey(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> {
// Получаем заголовки запроса
let headers = req.headers();
// Проверяем наличие и валидность API-ключа
let api_key = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| {
if value.starts_with("ApiKey ") {
Some(value[7..].to_string())
} else {
None
}
});
match api_key {
Some(key) if !key.is_empty() => Ok(ApiKey(key)),
_ => Err((StatusCode::UNAUTHORIZED, "Отсутствует или неверный API-ключ")),
}
}
}
// Использование кастомного экстрактора в хендлере
async fn protected_handler(
ApiKey(key): ApiKey,
) -> String {
format!("Доступ разрешен с ключом: {}", key)
}
let app = Router::new()
.route("/protected", get(protected_handler));
Вот еще несколько примеров кастомных экстракторов:
Экстрактор для проверки авторизации
struct AuthUser {
user_id: u64,
role: String,
}
#[async_trait]
impl<B> FromRequest<B> for AuthUser
where
B: Send,
{
type Rejection = (StatusCode, &'static str);
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получаем заголовки
let headers = req.headers();
// Получаем токен из заголовка Authorization
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| {
if value.starts_with("Bearer ") {
Some(value[7..].to_string())
} else {
None
}
})
.ok_or((StatusCode::UNAUTHORIZED, "Отсутствует токен"))?;
// Проверяем токен и получаем информацию о пользователе
// (в реальном приложении здесь была бы проверка через JWT, базу данных и т.д.)
if token == "valid_token" {
Ok(AuthUser {
user_id: 1,
role: "admin".to_string(),
})
} else {
Err((StatusCode::UNAUTHORIZED, "Недействительный токен"))
}
}
}
Экстрактор для валидации данных
use validator::Validate;
struct ValidatedJson<T>(T);
#[async_trait]
impl<B, T> FromRequest<B> for ValidatedJson<T>
where
B: Send,
T: DeserializeOwned + Validate + Send,
Json<T>: FromRequest<B, 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| (StatusCode::BAD_REQUEST, format!("Ошибка JSON: {}", err)))?;
// Затем валидируем данные
data.validate()
.map_err(|err| {
let error_message = err
.field_errors()
.iter()
.map(|(field, errors)| {
format!(
"Поле '{}': {}",
field,
errors[0].message.clone().unwrap_or_default()
)
})
.collect::<Vec<_>>()
.join(", ");
(StatusCode::BAD_REQUEST, error_message)
})?;
Ok(ValidatedJson(data))
}
}
Типобезопасные пути с экстракторами
Экстракторы особенно полезны для создания типобезопасных API с проверкой параметров на уровне компиляции:
use axum::{
extract::Path,
routing::get,
Router,
};
use uuid::Uuid;
use chrono::{DateTime, Utc};
// Типобезопасный UUID вместо строки
async fn get_user_by_uuid(
Path(user_id): Path<Uuid>,
) -> String {
format!("Получен пользователь с UUID: {}", user_id)
}
// Типобезопасная дата/время
async fn get_logs_by_date(
Path(date): Path<DateTime<Utc>>,
) -> String {
format!("Логи за: {}", date.format("%Y-%m-%d %H:%M:%S"))
}
let app = Router::new()
.route("/users/:user_id", get(get_user_by_uuid))
.route("/logs/:date", get(get_logs_by_date));
Лучшие практики использования экстракторов
1. Порядок экстракторов имеет значение
Порядок параметров в функции хендлера определяет порядок применения экстракторов. Рекомендуется использовать следующий порядок для оптимальной производительности:
State
— доступ к состоянию приложения- Экстракторы, которые не потребляют тело запроса (
Path
,Query
,HeaderMap
и т.д.) - Экстракторы, потребляющие тело запроса (
Json
,Form
,Multipart
)
async fn recommended_order(
State(state): State<AppState>, // Сначала состояние
Path(id): Path<u64>, // Затем параметры пути
Query(params): Query<Params>, // Затем query-параметры
headers: HeaderMap, // Затем заголовки
Json(payload): Json<Data>, // В конце тело запроса
) -> impl IntoResponse {
// ...
}
2. Используйте кастомные экстракторы для повторяющейся логики
Если у вас есть логика, которая повторяется в нескольких хендлерах (например, проверка авторизации, валидация данных), инкапсулируйте её в кастомный экстрактор.
3. Используйте обработку ошибок для улучшения UX
Предоставляйте понятные сообщения об ошибках при неудачной экстракции данных.
async fn create_user_with_feedback(
result: Result<Json<CreateUser>, JsonRejection>,
) -> impl IntoResponse {
match result {
Ok(Json(user)) => {
// Обработка данных...
(StatusCode::CREATED, "Пользователь создан")
},
Err(JsonRejection::JsonDataError(err)) => {
(
StatusCode::BAD_REQUEST,
format!("Ошибка в структуре JSON: {}", err)
)
},
Err(JsonRejection::JsonSyntaxError(err)) => {
(
StatusCode::BAD_REQUEST,
format!("Синтаксическая ошибка в JSON: {}", err)
)
},
Err(err) => {
(
StatusCode::BAD_REQUEST,
format!("Ошибка при обработке JSON: {}", err)
)
},
}
}
4. Используйте типобезопасные экстракторы
Предпочитайте строго типизированные экстракторы для раннего обнаружения ошибок:
// Вместо этого (строковый ID)
async fn get_user_string(Path(id): Path<String>) -> impl IntoResponse {
// Преобразование строки в число и обработка ошибок
let user_id = id.parse::<u64>().map_err(|_| StatusCode::BAD_REQUEST)?;
// ...
}
// Используйте это (типизированный ID)
async fn get_user_typed(Path(id): Path<u64>) -> impl IntoResponse {
// ID уже является числом
// ...
}
Заключение
Экстракторы — это мощный механизм Axum, который существенно упрощает работу с данными HTTP-запросов. Они позволяют писать чистый, декларативный код без необходимости вручную извлекать и преобразовывать данные из запросов. Используя встроенные экстракторы и создавая собственные, вы можете значительно повысить качество и поддерживаемость вашего кода.
В следующих разделах мы рассмотрим другие аспекты работы с Axum, такие как обработка JSON-данных, работа с формами, валидация входных данных и многое другое.