Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Мидлваре

Мидлваре (middleware) — функциональность, которая вызывается перед функцией-обработчиком, а также имеет возможность обработать результат функции обработчика после её завершения. Можно сказать, что мидлваре оборачивает вызов функции-обработчика запроса.

╭────╮     ┌──────┐ request ┌──────────┐ request ┌──────────┐
│    ├────▶│      ├────────▶│          ├────────▶│          │
│Сеть│ TCP │ Axum │         │middleware│         │обработчик│
│    │◀────┤роутер│◀────────┤          │◀────────┤ запроса  │
╰────╯     └──────┘ response└──────────┘ response└──────────┘

Если мы хотим выполнять какие-то действия перед обработкой любого запроса, то мидлваре — как раз тот механизм, который позволит нам это сделать.

При этом мы можем создать несколько мидлваре, которые будут цепочкой выполняться перед обработчиком запроса и после него.

    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
───▶│          ├───▶│          ├───▶│          ├───▶│          ├───▶
    │middleware│    │middleware│    │middleware│    │middleware│
◀───┤    1     │◀───┤    2     │◀───┤    3     │◀───┤    4     │◀───
    └──────────┘    └──────────┘    └──────────┘    └──────────┘

По диаграмме сверху видно, что объекты мидлваре выстраиваются подобно слоям, оборачивающим обработчик запроса. Именно поэтому зарегистрированное в роутере мидлваре называют слоем (layer).

Простейшее мидлваре

Самый простой вариант мидлваре выглядит как просто функция, которая принимает объект Request — HTTP запрос и объект Next — ссылка на следующий в цепочке мидлваре или обработчик запроса и возвращает объект Response, полученный от вызова обработчика запроса.

async fn мидлваре(request: Request, next: Next) -> Response {
    // ... действия перед обработкой запроса
    
    // вызываем следующий в цепочке обработчик
    let response = next.run(request).await;
    
    // ... действия после обработки запроса

    // возвращаем результат из мидлваре
    response
}

Такую функцию надо:

  • превратить в мидлваре при помощи функции from_fn
  • зарегистрировать в роутере при помощи метода layer

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

let app = Router::new()
    .route("/путь", get(функция-обработчик))
    .layer(middleware::from_fn(мидлваре));

Чтобы понять, как это работает, напишем классический пример мидлваре — мидлваре, который логирует время работы обработчика:

use axum::{Router, extract::Request, response::Response, routing::get};
use axum::middleware::{self, Next};
use tokio::time::Instant;

async fn log_exec_time(request: Request, next: Next) -> Response {
    let start = Instant::now();
    let response = next.run(request).await;
    tracing::info!("Request took: {} micros", start.elapsed().as_micros());
    response
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    let app = Router::new()
        .route("/hello", get(hello))
        .layer(middleware::from_fn(log_exec_time));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();

    axum::serve(listener, app).await.unwrap();
}

async fn hello() -> &'static str {
    "Hello!"
}

Запустим сервер:

$ cargo run

и перейдём в браузере на http://localhost:8080/hello.

В консоли должна появиться лог-запись с уровнем INFO от приложения test_axum.

2025-12-02T23:09:30.342625Z  INFO test_axum: Request took: 52 micros

Слои

Теперь давайте рассмотрим, как взаимодействуют между собой разные мидлваре.

Добавим в наш hello сервер два мидлваре, каждый из которых:

  • печатает в лог одно сообщение перед вызовом следующего обработчика по цепочке
  • печатает в лог другое сообщение после того, как следующий по цепочке обработчик закончил свою работу
use axum::middleware::{Next, from_fn};
use axum::{Router, extract::Request, routing::get};

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    let app = Router::new()
        .route("/hello", get(hello))
        .layer(from_fn(async |request: Request, next: Next| {
            tracing::info!("Middleware-1: before call");
            let response = next.run(request).await;
            tracing::info!("Middleware-1: after call");
            response
        }))
        .layer(from_fn(async |request: Request, next: Next| {
            tracing::info!("Middleware-2: before call");
            let response = next.run(request).await;
            tracing::info!("Middleware-2: after call");
            response
        }));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();

    axum::serve(listener, app).await.unwrap();
}

async fn hello() -> &'static str {
    tracing::info!("Request handler");
    "Hello!"
}

Запустим сервер и перейдём по адресу http://localhost:8080/hello

stas@slim:~/dev/proj/rust/test_axum$ cargo run
   Compiling test_axum v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.16s
     Running `target/debug/test_axum`

2025-12-03T22:36:42.563720Z  INFO test_axum: Middleware-2: before call
2025-12-03T22:36:42.563776Z  INFO test_axum: Middleware-1: before call
2025-12-03T22:36:42.563800Z  INFO test_axum: Request handler
2025-12-03T22:36:42.563823Z  INFO test_axum: Middleware-1: after call
2025-12-03T22:36:42.563834Z  INFO test_axum: Middleware-2: after call

Как видите, мидлваре оборачивают друг друга подобно матрёшке. При этом тот мидлваре, который был добавлен в роутер последним, становится первым в цепочке.

Работа со стэйтом

Нередко для логики работы мидлваре может понадобиться доступ к данным, хранящимся в состоянии. В этом случае функция-мидлваре будет иметь дополнительный аргумент — State.

async fn мидлваре(state: State<Стэйт>, request: Request, next: Next) -> Response {
    // Действия перед обработкой запроса
    let response = next.run(request).await;
    // действия после обработки запроса
    response
}

Такая функция превращается в мидлваре при помощи функции from_fn_with_state.

let app = Router::new()
    .route("/путь", get(функция-обработчик))
    .with_state(state.clone())
    .layer(middleware::from_fn_with_state(state, мидлваре));

Как вы могли заметить, состояние приходится передавать отдельно и в with_state, и в from_fn_with_state. С одной стороны, это не очень удобно, но, с другой стороны, позволяет иметь разные объекты состояния для мидлвари и для роутера.

В качестве примера напишем мидлваре, который извлекает из HTTP заголовка ID сессии, потом достаёт из состояния соответствующий объект сессии пользователя и помещает его в task-local переменную. Далее эта task-local переменная используется из функции-обработчика запроса.

use std::{collections::HashMap, sync::Arc};
use axum::{Router, extract::{Request, State}, http::StatusCode, routing::get};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use tokio::sync::{Mutex, RwLock};

tokio::task_local! {
    pub static SESSION: Arc<Mutex<SessionData>>;
}

// Сессия пользователям
struct SessionData {
    user_name: String,
}

// Состояние бекенда, хранящее сессии
struct AppState {
    sessions: RwLock<HashMap<String, Arc<Mutex<SessionData>>>>,
}

async fn set_session_for_request(
    State(state): State<Arc<AppState>>, request: Request, next: Next
) -> Response {
    // получаем объект сессии
    let session = if let Some(value) = request.headers().get("sessionid") {
        if let Ok(string_value) = value.to_str() {
            let session_id = string_value.to_string();
            let read_guard = state.sessions.read().await;
            if let Some(session) = read_guard.get(&session_id) {
                session.clone()
            } else {
                return StatusCode::UNAUTHORIZED.into_response();
            }
        } else {
            return StatusCode::BAD_REQUEST.into_response();
        }
    } else {
        return StatusCode::UNAUTHORIZED.into_response();
    };

    // Записываем объект сессии в task-local переменную
    let response = SESSION.scope(session, async {
            next.run(request).await // вызываем обработчик по цепочке
        }).await;
    response
}

#[tokio::main]
async fn main() {
    let sessions: RwLock<HashMap<String, Arc<Mutex<SessionData>>>> = {
        let mut data = HashMap::new();
        data.insert( // Создаём тестовую сессию пользователя
            "1111-1111-1111".to_string(),
            Arc::new(Mutex::new(SessionData {
                user_name: "John Doe".to_string(),
            })),
        );
        RwLock::new(data)
    };
    let app_state = Arc::new(AppState { sessions });

    let app = Router::new()
        .route("/hello", get(hello))
        .with_state(app_state.clone())
        .layer(middleware::from_fn_with_state(app_state, set_session_for_request));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();

    axum::serve(listener, app).await.unwrap();
}

async fn hello() -> String {
    // Извлекаем сессию из task-local переменной
    let session = SESSION.with(|session| session.clone());
    // используем сессию
    format!("Hello, {}!", session.lock().await.user_name)
}

Протестируем наш эндпоинт, чтобы убедиться, что мидлваре корректно помещает сессию в task-local переменную, доступную из функции-обработчика.

$ curl -i -H "sessionid: 1111-1111-1111" http://localhost:8080/hello

HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 15
date: Wed, 03 Dec 2025 14:03:22 GMT

Hello, John Doe!

Стандартные мидлваре

Экосистема Axum предоставляет ряд стандартных мидлваре для таких вещей, как CORS, компрессия, таймауты и т.д., но перед тем как мы посмотрим на то, как ими пользоваться, мы должны познакомиться с библиотекой Tower.