Обработчики
Обработчик запроса — это функция или замыкание, сопоставленные маршруту. Его аргументы — экстракторы: параметры маршрута и строки запроса, заголовки, JSON-тело, сервисы из DI, — а то, что он возвращает, превращается в ответ. Волга принимает обработчик в двух формах и определяет, какая из них перед ней, по сигнатуре.
Асинхронные обработчики
async fn или замыкание, возвращающее future. Это форма для обработчика, которому есть что ждать: запроса к базе данных, HTTP-вызова, чтения файла через tokio::fs.
use volga::{App, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/hello/{name}", |name: String| async move {
ok!("Hello, {name}!")
});
app.map_get("/users/{id}", get_user);
app.run().await
}
async fn get_user(id: u32) -> String {
// например, `db.find_user(id).await`
format!("user #{id}")
}
Синхронные обработчики
Начиная с v0.11.0 обработчик, которому нечего ждать, — форматирование, арифметика, поиск в памяти, проверка заголовка, — может быть обычной fn или замыканием, которое возвращает ответ напрямую:
use volga::{App, HttpResult, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/sum/{x}/{y}", |x: i32, y: i32| x + y);
app.map_get("/hello/{name}", |name: String| ok!("Hello, {name}!"));
app.map_get("/health", health);
app.run().await
}
fn health() -> HttpResult {
ok!("healthy")
}
Всё остальное — как у асинхронной формы: экстракторы выполняются до обработчика, поэтому Json<T>, Form<T>, Query<T> и Dc<T> приходят с уже прочитанным телом; возвращать можно всё, что реализует IntoResponse, включая HttpResult и Result<T, E>; в OpenAPI маршрут описывается так же.
use volga::{App, Json, ok};
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
name: String,
age: u32,
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_post("/users", |user: Json<User>| {
ok!("{} is {} years old", user.name, user.age)
});
app.run().await
}
Синхронный обработчик выполняется на том воркере рантайма, который опрашивает запрос, — ровно так же, как async-обработчик без единого .await внутри, и стоит столько же. Это подходящая форма для короткой работы, не нагружающей процессор.
Где принимаются обе формы
map_get,map_post,map_put,map_patch,map_delete,map_head,map_options,map_trace,map_queryиmap— уAppи уRouteGroup;map_fallbackиmap_err;- WebSocket-методы
map_connиmap_msg; - middleware
filter,map_ok,map_errиtap_req.
wrap, with и attach принимают только асинхронные middleware: они существуют ради того, чтобы дождаться next.
Возврат ошибок
Обработчик, который может завершиться неудачей, возвращает Result<T, E>: Ok — это ответ, а начиная с 0.12.0 Err — ошибка, которая передаётся обработчику ошибок и никогда не становится самостоятельным ответом. E — это Error из Volga, StatusCode, пара (StatusCode, сообщение), std::io::Error или любой тип, реализующий IntoError, — и все они преобразуются через ?.
use volga::{App, HttpResult, http::StatusCode, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/users/{id}", |id: u32| -> Result<String, StatusCode> {
match id {
1 => Ok("admin".into()),
_ => Err(StatusCode::NOT_FOUND),
}
});
app.map_get("/config", read_config);
app.run().await
}
async fn read_config() -> HttpResult {
// Отсутствующий файл — это 404, нечитаемый — 403
let text = tokio::fs::read_to_string("app_config.toml").await?;
ok!(text)
}
Примечание
Голое сообщение — Err("not found"), Err(format!(..)) — не несёт статуса и отвечает 500. Дайте ему статус: Err((StatusCode::NOT_FOUND, "not found")).
Блокирующая работа
Синхронный обработчик, который действительно блокирует, — std::fs, синхронный драйвер базы данных, долгое вычисление, — не должен выполняться на воркере рантайма: пока он работает, этот воркер больше ничего не опрашивает. Оберните его в blocking, и его тело будет выполняться в блокирующем пуле Tokio:
use volga::{App, HttpResult, blocking, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/reports/{id}", blocking(|id: u32| -> HttpResult {
let report = std::fs::read_to_string(format!("reports/{id}.txt"))?;
ok!(report)
}));
app.run().await
}
- Экстракторы по-прежнему выполняются на воркере; в
spawn_blockingпереносится и там ожидается только тело. blockingпринимает синхронный обработчик. Асинхронному переносить нечего — он и так уступает управление, — и компилятор его отклонит.- Обработчик разделяется между запросами, а не клонируется для каждого, поэтому захваченное им не обязано быть
Clone. - Паника в теле возобновляется в задаче, которая его ожидает, — так же, как если бы обработчик выполнялся на месте.
- Передача работы в другой поток стоит гораздо больше, чем короткое тело, поэтому используйте
blockingтолько тогда, когда тело действительно блокирует.
Отмена
Вынесенный вызов не отменяется вместе с запросом: начавшись, он выполняется до конца, а его результат отбрасывается, если клиент уже ушёл. Долгое тело, которое должно останавливаться раньше, принимает CancellationToken и проверяет его по ходу работы:
use std::time::Duration;
use volga::{App, CancellationToken, blocking};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/blocking-task", blocking(|token: CancellationToken| {
for _ in 0..5 {
if token.is_cancelled() {
break;
}
std::thread::sleep(Duration::from_secs(1));
}
"done"
}));
app.run().await
}
Явные generic-параметры
В коде обработчика форма нигде не называется: её несёт тип-маркер, marker::Async или marker::Immediate, который выводит компилятор. Это последний generic-параметр каждого метода, регистрирующего обработчик, поэтому там, где generic-параметры указаны явно, для него оставляют _:
app.map_get::<_, _, (i32, i32), _>("/sum/{x}/{y}", |x: i32, y: i32| x + y);
В собственном ограничении F: GenericHandler<Args> означает асинхронную форму, поскольку маркер по умолчанию — marker::Async; F: GenericHandler<Args, M> с обобщённым M принимает обе.
Тип, который одновременно является Future и IntoResponse, подходит под обе формы и не компилируется как тип результата обработчика. В самой Волге таких типов нет.