Параметризованные Middleware
Помимо встроенных middleware-замыканий, регистрируемых через wrap() и with(), Volga также поддерживает регистрацию middleware в виде повторно используемых настраиваемых типов через метод attach().
Такой подход удобен, когда middleware требует собственного состояния, конфигурации или должен использоваться повторно в нескольких приложениях.
Обзор
Параметризованный middleware — это обычный Rust-тип (как правило, struct), реализующий трейт Middleware. Такой тип хранит всю необходимую конфигурацию и разделяемое состояние, а его метод call() содержит основную логику middleware.
Это похоже на паттерны middleware в других экосистемах (например, слои Tower, middleware ASP.NET Core или классы Express.js).
Трейт Middleware
Трейт определён следующим образом:
pub trait Middleware: Send + Sync + 'static {
fn call(
&self,
ctx: HttpContext,
next: NextFn,
) -> impl Future<Output = HttpResult> + Send + 'static;
}
Любой тип, реализующий этот трейт, можно передать в attach().
Пример: Middleware Timeout
Ниже приведён небольшой middleware, добавляющий искусственную задержку перед дальнейшей обработкой запроса. Длительность задержки настраивается при регистрации:
use std::time::Duration;
use volga::{App, HttpResult, middleware::{HttpContext, NextFn, Middleware}};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Регистрируем параметризованный middleware
app.attach(Timeout {
duration: Duration::from_secs(1),
});
app.map_get("/hello", || async { "Hello, World!" });
app.run().await
}
struct Timeout {
duration: Duration,
}
impl Middleware for Timeout {
fn call(&self, ctx: HttpContext, next: NextFn) -> impl Future<Output = HttpResult> + 'static {
let duration = self.duration;
async move {
tokio::time::sleep(duration).await;
next(ctx).await
}
}
}
Структура Timeout хранит свою конфигурацию (duration) и реализует логику middleware внутри call(). Можно создать несколько экземпляров с разными значениями задержки или переиспользовать один экземпляр для нескольких маршрутов.
.wrap() и .attach()
Оба метода регистрируют middleware, работающие с полным HttpContext, но рассчитаны на разные сценарии:
wrap()оптимизирован для коротких встроенных замыканий. Аннотации типов дляctxиnextне требуются.attach()предназначен для повторно используемых параметризованных типов middleware — как правило, структур, реализующих трейтMiddleware.
Совет
Используйте wrap() для быстрых встроенных middleware и attach(), когда нужно оформить middleware как именованный настраиваемый тип, пригодный для повторного использования.
attach() также принимает замыкания, но в этом случае требуется указывать аннотации типов для аргументов:
use volga::{App, middleware::{HttpContext, NextFn}};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.attach(|ctx: HttpContext, next: NextFn| async move {
next(ctx).await
});
app.run().await
}
Регистрация на маршрутах и группах маршрутов
Параметризованные middleware могут быть прикреплены не только ко всему приложению, но и к отдельным маршрутам и группам маршрутов:
use std::time::Duration;
use volga::{App, HttpResult, middleware::{HttpContext, NextFn, Middleware}};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Прикрепление к одному маршруту
app
.map_get("/hello", || async { "Hello, World!" })
.attach(Timeout { duration: Duration::from_secs(1) });
// Прикрепление к группе маршрутов
app.group("/api", |api| {
api.attach(Timeout { duration: Duration::from_secs(2) });
api.map_get("/ping", || async { "pong" });
});
app.run().await
}
struct Timeout {
duration: Duration,
}
impl Middleware for Timeout {
fn call(&self, ctx: HttpContext, next: NextFn) -> impl Future<Output = HttpResult> + 'static {
let duration = self.duration;
async move {
tokio::time::sleep(duration).await;
next(ctx).await
}
}
}
Когда использовать параметризованные Middleware
Выбирайте attach() и отдельный тип, если:
- Middleware требует конфигурации при регистрации (таймауты, лимиты, feature flags и т. п.).
- Middleware должен быть переиспользуемым между проектами или крейтами.
- Middleware хранит разделяемое состояние, счётчики или дескрипторы внешних систем.
- Вы хотите юнит-тестировать middleware независимо от работающего приложения.
Для простых разовых преобразований, как правило, лаконичнее оставить логику встроенной через wrap() или with().
Встроенные возможности, такие как CORS, аутентификация и ограничение частоты запросов, сами реализованы как параметризованные middleware поверх attach().
Другие варианты Middleware
Тот же параметризованный подход работает и для остальных middleware-трейтов. Помимо Middleware, вы можете реализовать Filter, TapReq, MapOk, MapErr и With на собственных типах. Они регистрируются теми же методами, что и их аналоги-замыкания — filter(), tap_req(), map_ok(), map_err() и with() соответственно. Например, переиспользуемый параметризованный фильтр:
use volga::{App, headers::HttpHeaders, middleware::Filter};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/sum/{x}/{y}", |x: i32, y: i32| async move { x + y })
.filter(HasHeader {
header: "x-api-key".to_owned()
});
app.run().await
}
#[derive(Clone)]
struct HasHeader {
header: String
}
impl Filter<HttpHeaders> for HasHeader {
type Output = bool;
async fn filter(&self, headers: HttpHeaders) -> bool {
headers.get_raw(&self.header).is_some()
}
}