Запросы и ответы
В этом разделе мы рассмотрим, как Axum обрабатывает HTTP-запросы и формирует ответы. Мы изучим различные способы доступа к данным запроса и создания кастомизированных ответов, а также познакомимся с механизмами обработки HTTP-заголовков, статус-кодов и других аспектов HTTP-протокола.
Работа с HTTP-запросами
Структура HTTP-запроса
HTTP-запрос состоит из следующих основных компонентов:
- HTTP-метод (GET, POST, PUT и т.д.)
- URL-путь и параметры запроса
- HTTP-заголовки
- Тело запроса (опционально)
Axum предоставляет различные способы доступа к каждому из этих компонентов.
Использование низкоуровневых экстракторов
Для прямого доступа к компонентам HTTP-запроса в Axum есть несколько встроенных экстракторов:
use axum::{
extract::{Request, Path, Query},
http::{Method, HeaderMap, Uri},
};
use serde::Deserialize;
// Получение всего запроса
async fn handler_with_request(req: Request) -> String {
format!("Получен запрос: {:?}", req)
}
// Извлечение HTTP-метода
async fn handler_with_method(method: Method) -> String {
format!("HTTP-метод: {}", method)
}
// Доступ к URL
async fn handler_with_uri(uri: Uri) -> String {
format!("URL-путь: {}", uri.path())
}
// Доступ к заголовкам
async fn handler_with_headers(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)
}
// Комбинирование нескольких экстракторов
#[derive(Deserialize)]
struct Params {
filter: Option<String>,
}
async fn complex_handler(
method: Method,
uri: Uri,
headers: HeaderMap,
Path(id): Path<u64>,
Query(params): Query<Params>,
) -> String {
format!(
"Метод: {}, Путь: {}, ID: {}, Фильтр: {}, User-Agent: {}",
method,
uri.path(),
id,
params.filter.unwrap_or_default(),
headers.get("user-agent")
.map(|v| v.to_str().unwrap_or_default())
.unwrap_or_default()
)
}
Доступ к телу запроса
Axum предоставляет несколько способов доступа к телу запроса:
use axum::{
extract::{Json, Form, Bytes},
body::Body,
http::StatusCode,
};
use serde::Deserialize;
// Извлечение тела запроса как JSON
#[derive(Deserialize)]
struct User {
name: String,
email: String,
}
async fn create_user_json(
Json(user): Json<User>,
) -> String {
format!("Создан пользователь: {} ({})", user.name, user.email)
}
// Извлечение тела запроса как формы
async fn create_user_form(
Form(user): Form<User>,
) -> String {
format!("Создан пользователь из формы: {} ({})", user.name, user.email)
}
// Извлечение сырых байтов
async fn raw_body_handler(
bytes: Bytes,
) -> String {
format!("Получено {} байт данных", bytes.len())
}
// Получение всего тела как axum::body::Body
async fn body_handler(
body: Body,
) -> String {
let bytes = axum::body::to_bytes(body, usize::MAX)
.await
.unwrap_or_default();
format!("Получено {} байт данных", bytes.len())
}
Работа с multipart/form-data
Для обработки загрузки файлов и multipart-форм в Axum обычно используется крейт axum-multipart
:
use axum::{
extract::Multipart,
http::StatusCode,
};
use std::io::Write;
async fn upload_handler(mut multipart: Multipart) -> Result<String, StatusCode> {
let mut uploaded_files = 0;
let mut total_bytes = 0;
while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::BAD_REQUEST)? {
let name = field.name().unwrap_or_default().to_string();
let file_name = field.file_name().unwrap_or_default().to_string();
let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?;
total_bytes += data.len();
// Пример сохранения файла
if !file_name.is_empty() {
let path = format!("./uploads/{}", file_name);
let mut file = std::fs::File::create(&path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
file.write_all(&data).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
uploaded_files += 1;
}
}
Ok(format!("Загружено {} файлов, общий размер: {} байт", uploaded_files, total_bytes))
}
Формирование HTTP-ответов
Основы формирования ответов
В Axum все, что возвращает хендлер, должно реализовывать типаж IntoResponse
. Это позволяет гибко формировать HTTP-ответы:
use axum::{
response::IntoResponse,
http::{StatusCode, HeaderMap, HeaderValue},
Json,
};
use serde::Serialize;
// Простой текстовый ответ
async fn text_response() -> &'static str {
"Это текстовый ответ"
}
// Ответ с JSON-данными
#[derive(Serialize)]
struct ApiResponse {
message: String,
data: Vec<String>,
}
async fn json_response() -> Json<ApiResponse> {
Json(ApiResponse {
message: "Успешно".to_string(),
data: vec!["один".to_string(), "два".to_string()],
})
}
// Ответ со статус-кодом
async fn not_found() -> StatusCode {
StatusCode::NOT_FOUND
}
// Ответ со статус-кодом и телом
async fn bad_request() -> (StatusCode, &'static str) {
(StatusCode::BAD_REQUEST, "Некорректный запрос")
}
// Комплексный ответ с заголовками
async fn complex_response() -> impl IntoResponse {
let mut headers = HeaderMap::new();
headers.insert("X-Custom-Header", HeaderValue::from_static("custom-value"));
headers.insert("Cache-Control", HeaderValue::from_static("max-age=3600"));
(
StatusCode::OK,
headers,
Json(ApiResponse {
message: "Расширенный ответ".to_string(),
data: vec!["с".to_string(), "заголовками".to_string()],
}),
)
}
Реализация IntoResponse для кастомных типов
Для более сложных случаев полезно реализовать типаж IntoResponse
для собственных типов:
use axum::{
response::{IntoResponse, Response},
http::{StatusCode, header},
Json,
};
use serde::Serialize;
// Кастомный тип ответа API
#[derive(Serialize)]
struct ApiSuccessResponse<T>
where
T: Serialize,
{
success: bool,
data: T,
message: String,
}
// Кастомный тип ответа с ошибкой
#[derive(Serialize)]
struct ApiErrorResponse {
success: bool,
error: String,
code: String,
}
// Enum для представления результата API
enum ApiResult<T>
where
T: Serialize,
{
Success(T, String),
Error(StatusCode, String, String),
}
impl<T> IntoResponse for ApiResult<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
match self {
ApiResult::Success(data, message) => {
let response = ApiSuccessResponse {
success: true,
data,
message,
};
(StatusCode::OK, Json(response)).into_response()
},
ApiResult::Error(status, error, code) => {
let response = ApiErrorResponse {
success: false,
error,
code,
};
(status, Json(response)).into_response()
}
}
}
}
// Использование кастомного типа ответа
async fn api_handler(Path(id): Path<u64>) -> ApiResult<User> {
match find_user(id).await {
Some(user) => ApiResult::Success(user, "Пользователь найден".to_string()),
None => ApiResult::Error(
StatusCode::NOT_FOUND,
"Пользователь не найден".to_string(),
"USER_NOT_FOUND".to_string(),
),
}
}
Управление заголовками ответа
Axum позволяет гибко управлять HTTP-заголовками ответа:
use axum::{
response::{IntoResponse, Response},
http::{StatusCode, header, HeaderMap, HeaderValue},
};
// Добавление заголовков через кортеж
async fn with_headers() -> impl IntoResponse {
(
StatusCode::OK,
[
(header::CONTENT_TYPE, "text/plain"),
(header::CACHE_CONTROL, "max-age=3600"),
("X-Custom-Header", "custom-value"),
],
"Ответ с заголовками"
)
}
// Модификация заголовков через HeaderMap
async fn with_header_map() -> impl IntoResponse {
let body = "Ответ с заголовками через HeaderMap";
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("max-age=3600"));
headers.insert("X-Custom-Header", HeaderValue::from_static("custom-value"));
(StatusCode::OK, headers, body)
}
// Добавление конкретных заголовков через extensions
async fn set_cookies() -> impl IntoResponse {
let mut response = Response::builder()
.status(StatusCode::OK)
.body("Ответ с куками")
.unwrap();
let headers = response.headers_mut();
headers.insert(
header::SET_COOKIE,
HeaderValue::from_str("session=abc123; Path=/; HttpOnly; SameSite=Lax").unwrap(),
);
headers.insert(
header::SET_COOKIE,
HeaderValue::from_str("theme=dark; Path=/; Max-Age=31536000").unwrap(),
);
response
}
Потоковые ответы
Axum поддерживает потоковую передачу ответов, что полезно для больших файлов или SSE (Server-Sent Events):
use axum::{
response::{IntoResponse, Response, sse::{Event, Sse}},
extract::Path,
body::StreamBody,
};
use futures::{Stream, stream};
use tokio::fs::File;
use tokio_util::io::ReaderStream;
use std::{convert::Infallible, time::Duration};
// Потоковая передача файла
async fn stream_file(Path(filename): Path<String>) -> Result<impl IntoResponse, StatusCode> {
let path = format!("./files/{}", filename);
let file = File::open(path).await.map_err(|_| StatusCode::NOT_FOUND)?;
// Преобразование файла в поток
let stream = ReaderStream::new(file);
let body = StreamBody::new(stream);
let headers = [
(header::CONTENT_TYPE, "application/octet-stream"),
(header::CONTENT_DISPOSITION, &format!("attachment; filename=\"{}\"", filename)),
];
Ok((headers, body))
}
// Server-Sent Events (SSE)
async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let stream = stream::repeat_with(|| {
let timestamp = chrono::Utc::now().to_rfc3339();
Event::default().data(format!("Current time: {}", timestamp))
})
.map(Ok)
.throttle(Duration::from_secs(1));
Sse::new(stream)
}
Обработка ошибок
Базовая обработка ошибок
Axum поддерживает возврат Result<T, E>
из хендлеров, где T
и E
реализуют IntoResponse
:
use axum::{
response::IntoResponse,
http::StatusCode,
Json,
};
use serde::{Serialize, Deserialize};
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
// Простая обработка ошибок
async fn get_user(Path(id): Path<u64>) -> Result<Json<User>, StatusCode> {
if id == 42 {
let user = User {
id,
name: "Джон Доу".to_string(),
};
Ok(Json(user))
} else {
Err(StatusCode::NOT_FOUND)
}
}
Кастомные типы ошибок
Для более детальной обработки ошибок удобно создать собственный тип ошибки:
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
Json,
};
use serde::Serialize;
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput(String),
DatabaseError(String),
Unauthorized(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (message, _) = self.get_details();
write!(f, "{}", message)
}
}
impl AppError {
fn get_details(&self) -> (String, StatusCode) {
match self {
AppError::NotFound(message) => (message.clone(), StatusCode::NOT_FOUND),
AppError::InvalidInput(message) => (message.clone(), StatusCode::BAD_REQUEST),
AppError::DatabaseError(message) => (message.clone(), StatusCode::INTERNAL_SERVER_ERROR),
AppError::Unauthorized(message) => (message.clone(), StatusCode::UNAUTHORIZED),
}
}
}
#[derive(Serialize)]
struct ErrorResponse {
success: bool,
error: String,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (message, status) = self.get_details();
// Логирование ошибки
tracing::error!("{}: {}", status, message);
// Формирование JSON-ответа
let body = Json(ErrorResponse {
success: false,
error: message,
});
(status, body).into_response()
}
}
// Использование в хендлере
async fn create_user(
Json(payload): Json<CreateUser>,
) -> Result<Json<User>, AppError> {
// Валидация
if payload.name.is_empty() {
return Err(AppError::InvalidInput("Имя не может быть пустым".to_string()));
}
// Создание пользователя в БД
match db::create_user(&payload).await {
Ok(user) => Ok(Json(user)),
Err(e) => Err(AppError::DatabaseError(format!("Ошибка БД: {}", e))),
}
}
Обработка ошибок с использованием thiserror и anyhow
Для более удобной работы с ошибками можно использовать популярные крейты thiserror
и anyhow
:
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
Json,
};
use serde::Serialize;
use thiserror::Error;
#[derive(Error, Debug)]
enum ApiError {
#[error("Ресурс не найден: {0}")]
NotFound(String),
#[error("Неверные входные данные: {0}")]
BadRequest(String),
#[error("Ошибка авторизации: {0}")]
Unauthorized(String),
#[error("Внутренняя ошибка сервера")]
Internal(#[from] anyhow::Error),
#[error("Ошибка базы данных: {0}")]
Database(#[from] sqlx::Error),
}
#[derive(Serialize)]
struct ErrorPayload {
message: String,
code: String,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, code, message) = match &self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "NOT_FOUND", msg),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg),
ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED", msg),
ApiError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"Внутренняя ошибка сервера"
),
ApiError::Database(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"DATABASE_ERROR",
"Ошибка базы данных"
),
};
// Детальное логирование для внутренних ошибок
if let ApiError::Internal(ref e) = self {
tracing::error!("Внутренняя ошибка: {:?}", e);
}
let payload = ErrorPayload {
message: message.to_string(),
code: code.to_string(),
};
(status, Json(payload)).into_response()
}
}
// Использование в хендлере
async fn get_user(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<User>, ApiError> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&state.pool)
.await?; // Автоматически конвертируется в ApiError::Database
user.map(Json)
.ok_or_else(|| ApiError::NotFound(format!("Пользователь с ID {} не найден", id)))
}
Управление кодами состояния
Определение кодов состояния (статус-кодов)
Axum предоставляет удобный способ установки HTTP-статус-кодов через тип StatusCode
:
use axum::{
http::StatusCode,
response::IntoResponse,
};
async fn created() -> (StatusCode, &'static str) {
(StatusCode::CREATED, "Ресурс успешно создан")
}
async fn accepted() -> (StatusCode, &'static str) {
(StatusCode::ACCEPTED, "Запрос принят в обработку")
}
async fn no_content() -> StatusCode {
StatusCode::NO_CONTENT
}
// Перенаправление
async fn redirect() -> impl IntoResponse {
(
StatusCode::FOUND,
[(header::LOCATION, "/new-location")],
"Перенаправление..."
)
}
Обработка различных кодов состояния
Axum также позволяет обрабатывать различные статус-коды на основе логики приложения:
async fn conditional_response(
Query(params): Query<Params>,
) -> impl IntoResponse {
match params.action.as_deref() {
Some("create") => (StatusCode::CREATED, "Ресурс создан"),
Some("update") => (StatusCode::OK, "Ресурс обновлен"),
Some("delete") => (StatusCode::NO_CONTENT, ""),
Some("move") => (
StatusCode::FOUND,
[(header::LOCATION, "/new-location")],
"Перенаправление..."
),
_ => (StatusCode::BAD_REQUEST, "Неизвестное действие"),
}
}
Лучшие практики
Структурирование API-ответов
Для обеспечения согласованности API рекомендуется использовать стандартные форматы ответов:
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
Json,
};
use serde::Serialize;
// Общая структура успешного ответа
#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: T,
message: Option<String>,
}
// Общая структура ответа с ошибкой
#[derive(Serialize)]
struct ApiErrorResponse {
success: bool,
error: String,
code: String,
}
// Функция-помощник для успешных ответов
fn success<T: Serialize>(data: T, message: Option<String>) -> Json<ApiResponse<T>> {
Json(ApiResponse {
success: true,
data,
message,
})
}
// Функция-помощник для ответов с ошибкой
fn error(status: StatusCode, error: String, code: String) -> impl IntoResponse {
let response = ApiErrorResponse {
success: false,
error,
code,
};
(status, Json(response))
}
// Использование в хендлерах
async fn get_user(Path(id): Path<u64>) -> impl IntoResponse {
match find_user(id).await {
Some(user) => (
StatusCode::OK,
success(user, Some("Пользователь найден".to_string()))
),
None => error(
StatusCode::NOT_FOUND,
"Пользователь не найден".to_string(),
"USER_NOT_FOUND".to_string()
),
}
}
Валидация запросов
Рекомендуется валидировать входящие данные перед их обработкой:
use axum::{
extract::Json,
http::StatusCode,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationError};
#[derive(Debug, Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3, message = "Имя должно содержать минимум 3 символа"))]
name: String,
#[validate(email(message = "Некорректный email"))]
email: String,
#[validate(length(min = 8, message = "Пароль должен содержать минимум 8 символов"))]
password: String,
}
async fn create_user(
Json(payload): Json<CreateUser>,
) -> Result<impl IntoResponse, impl IntoResponse> {
// Валидация входных данных
if let Err(validation_errors) = payload.validate() {
let error_message = validation_errors
.field_errors()
.iter()
.map(|(field, errors)| {
let error_message = errors[0].message.clone().unwrap_or_else(|| {
format!("Поле '{}' содержит ошибки", field)
});
error_message.to_string()
})
.collect::<Vec<_>>()
.join(", ");
return Err(error(
StatusCode::BAD_REQUEST,
error_message,
"VALIDATION_ERROR".to_string()
));
}
// Обработка данных...
Ok((
StatusCode::CREATED,
success(
User { id: 1, name: payload.name, email: payload.email },
Some("Пользователь создан".to_string())
)
))
}
Обработка больших нагрузок
Для приложений с высокой нагрузкой важно оптимизировать работу с запросами и ответами:
use std::time::Duration;
use tower::timeout::TimeoutLayer;
use tower_http::{
compression::CompressionLayer,
limit::RequestBodyLimitLayer,
};
// Настройка приложения для высокой нагрузки
let app = Router::new()
.route("/api/users", get(list_users))
// Ограничение размера тела запроса (10 МБ)
.layer(RequestBodyLimitLayer::new(10 * 1024 * 1024))
// Сжатие ответов
.layer(CompressionLayer::new())
// Таймаут запросов
.layer(TimeoutLayer::new(Duration::from_secs(30)));
Заключение
В этом разделе мы рассмотрели различные аспекты работы с HTTP-запросами и ответами в Axum. Мы изучили, как извлекать данные из запросов, формировать и кастомизировать ответы, обрабатывать ошибки и управлять HTTP-заголовками и статус-кодами.
Axum предоставляет мощные и типобезопасные абстракции для работы с HTTP, которые позволяют создавать надежные веб-приложения и API с чистым и поддерживаемым кодом.
В следующем разделе мы более подробно рассмотрим систему экстракторов, которая является одной из ключевых особенностей Axum.