Основы Middleware
Volga предоставляет инструменты для построения цепочек промежуточной обработки (middleware) — как для отдельных маршрутов, так и для групп маршрутов или всего приложения.
Включение поддержки middleware
Если вы не используете полный набор функций full, необходимо явно включить фичу middleware в Cargo.toml:
[dependencies]
volga = { version = "...", features = ["middleware"] }
Фильтры
Метод filter() позволяет задать условную логику (например, проверку прав или валидацию) для конкретного маршрута или группы маршрутов.
use volga::{App, Path};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Разрешаем только положительные числа
app.group("/positive", |g| {
g.filter(|Path((x, y)): Path<(i32, i32)>| async move { x >= 0 && y >= 0 });
g.map_get("/sum/{x}/{y}", sum);
});
// Разрешаем только отрицательные числа
app.map_get("/negative/sum/{x}/{y}", sum)
.filter(|Path((x, y)): Path<(i32, i32)>| async move { x < 0 && y < 0 });
app.run().await
}
async fn sum(x: i32, y: i32) -> i32 {
x + y
}
Что возвращает фильтр
Вердикт фильтра — это bool, (), FilterResult или Result<(), E>. Остановленный фильтром запрос уходит в обработчик ошибок со статусом того, что вернул фильтр:
| Фильтр возвращает | Ответ |
|---|---|
false или FilterResult::err() | 400 Bad Request с сообщением Request rejected by filter |
Err чего угодно со статусом — Error, StatusCode, (StatusCode, E), std::io::Error, ValidationError, OAuthError, Problem, ваш тип, реализующий IntoError | этот статус — тот же, что получил бы такой Err из обработчика |
Err строки или Box<dyn std::error::Error + Send + Sync> | 400 Bad Request с этим сообщением |
Фильтр отклоняет запрос по любой причине — из-за отсутствующего API-ключа так же часто, как из-за неверного параметра, — поэтому сообщение для false никакой причины не называет. До 0.14.0 оно звучало как Validation: One or more request parameters are incorrect, а поскольку глобальный фильтр решает судьбу и запросов, для которых не нашлось маршрута, то же самое получал и запрос к несуществующему пути. Фильтр, которому нужен собственный статус или сообщение, возвращает Err.
Последняя строка — единственное отличие от обработчика, где строка даёт 500: ошибка без собственного статуса, возвращённая фильтром, считается причиной отказа в запросе. Начиная с 0.12.0 ошибка со статусом его сохраняет — OAuthError отвечает 401 для invalid_token и 403 для insufficient_scope, а Error сохраняет свой instance и прикреплённый ответ.
use volga::{App, headers::HttpHeaders, http::StatusCode};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// 401 Unauthorized без заголовка
app.filter(|headers: HttpHeaders| match headers.get_raw("x-api-key") {
Some(_) => Ok(()),
None => Err((StatusCode::UNAUTHORIZED, "missing API key")),
});
app.map_get("/sum/{x}/{y}", |x: i32, y: i32| x + y);
app.run().await
}
FilterResult::with_error() принимает те же типы и отвечает так же, как Err с этой ошибкой.
Одного std::error::Error недостаточно
Err фильтра должен быть Error или реализовывать IntoError. Тип, который реализует только std::error::Error, — ParseIntError, ошибка из другого крейта — там не компилируется; дайте ему статус через .map_err(|err| (StatusCode::BAD_REQUEST, err)) или оберните в собственный тип ошибки, реализующий IntoError.
Обработка входящего запроса
Метод tap_req() позволяет получить доступ к HttpRequestMut, чтобы модифицировать или исследовать запрос до обработки.
use volga::{App, HttpRequestMut, headers};
headers! {
(CustomHeader, "x-custom-header")
}
#[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 })
.tap_req(|mut req: HttpRequestMut| async move {
req.try_insert_header::<CustomHeader>("Custom Value")?;
Ok(req)
});
app.run().await
}
Обработка успешного ответа
Метод map_ok() позволяет изменить или дополнить HTTP-ответ после успешной обработки.
use volga::{App, HttpResponse, HttpResult, headers};
headers! {
(CustomHeader, "x-custom-header")
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.group("/positive", |g| {
g.map_ok(group_response);
g.map_get("/sum/{x}/{y}", sum);
});
app.map_get("/negative/sum/{x}/{y}", sum)
.map_ok(route_response);
app.run().await
}
async fn group_response(mut resp: HttpResponse) -> HttpResult {
resp.try_insert_header::<CustomHeader>("for-group")?;
Ok(resp)
}
async fn route_response(mut resp: HttpResponse) -> HttpResult {
resp.try_insert_header::<CustomHeader>("for-route")?;
Ok(resp)
}
async fn sum(x: i32, y: i32) -> i32 {
x + y
}
Обработка ошибок
Метод map_err() позволяет определить кастомную логику обработки ошибок — глобально, для отдельного маршрута или для группы.
use volga::{App, error::{Error, ProblemDetails}};
use std::io::Error as IoError;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/error", produce_err)
.map_err(handle_err);
app.run().await
}
async fn handle_err(error: Error) -> ProblemDetails {
ProblemDetails::from(error)
}
async fn produce_err() -> IoError {
IoError::other("some error")
}
Начиная с 0.12.0 любой Err, который возвращает обработчик, доходит до этого map_err как Error — включая Err(StatusCode::NOT_FOUND) и Err вашего собственного типа ошибки.
Совет
Если вызвать map_err() у App, вы настроите глобальный обработчик ошибок. Подробнее о глобальной обработке ошибок читайте здесь.
Синхронные middleware
Начиная с v0.11.0 filter(), map_ok(), map_err() и tap_req() принимают также обычную fn или замыкание, которое возвращает вердикт, ответ или запрос напрямую, когда ждать нечего, — у App, маршрута и группы маршрутов, так же как и обработчики:
use volga::{App, HttpResponse, HttpResult, Path, headers, headers::HttpHeaders};
headers! {
(CustomHeader, "x-custom-header")
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Отклонять запросы без API-ключа
app.filter(|headers: HttpHeaders| headers.get_raw("x-api-key").is_some());
app.map_get("/positive/sum/{x}/{y}", |x: i32, y: i32| x + y)
.filter(|Path((x, y)): Path<(i32, i32)>| x >= 0 && y >= 0)
.map_ok(tag_response);
app.run().await
}
fn tag_response(mut resp: HttpResponse) -> HttpResult {
resp.try_insert_header::<CustomHeader>("sync")?;
Ok(resp)
}
wrap(), with() и attach() остаются асинхронными: они существуют ради того, чтобы дождаться next.