Сырое тело запроса
Большинство обработчиков используют типизированные экстракторы — Json<T>, Form<T>, Multipart — и никогда не работают с байтами напрямую. Но когда байты всё же нужны (прокси, вебхук, подпись которого покрывает точное содержимое, свой бинарный формат), Волга даёт доступ к телу запроса как есть.
Чтение тела в байты
Начиная с v0.9.4, HttpBody является экстрактором — достаточно объявить его аргументом обработчика, чтобы получить сырое тело запроса:
[dependencies]
volga = { version = "..." }
tokio = { version = "...", features = ["full"] }
http-body-util = "0.1"
use http_body_util::BodyExt;
use volga::{App, HttpBody, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_post("/raw", |body: HttpBody| async move {
let bytes = body.collect().await?.to_bytes();
ok!(format!("received {} bytes", bytes.len()))
});
app.run().await
}
Для сборки тела нужны методы трейта Body, которые приходят из расширяющего трейта BodyExt крейта http-body-util — Волга его не реэкспортирует.
Примечание
Тело запроса — это поток, который можно прочитать лишь однажды, поэтому HttpBody нельзя комбинировать с другим экстрактором тела в одном обработчике. Экстракторы, читающие только заголовочную часть запроса — Path<T>, Query<T>, HttpHeaders, Dc<T> — сочетаются с ним свободно.
Совет
Ограничение на размер тела продолжает действовать: по умолчанию Волга отвечает 413 Content Too Large на тело больше 5 МБ, как бы обработчик его ни читал. Как задать его для приложения, группы маршрутов или маршрута — в разделе Ограничение размера тела.
Проброс тела дальше
HttpBody можно и возвращать, поэтому обработчик-транзит сводится к перемещению тела в ответ — ничего не буферизуется:
use volga::{App, HttpBody, HttpResponse, headers::ContentType};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_post("/trace", |body: HttpBody| async move {
HttpResponse::builder()
.status(200)
.header(ContentType::multipart_form_data("X-BOUNDARY"))
.body(body)
});
app.run().await
}
Потоковая обработка тела
Чтобы обрабатывать тело по мере поступления, а не собирать его целиком, используйте HttpBodyStream — это ByteStream поверх тела запроса, реализующий Stream<Item = Result<Bytes, Error>>:
[dependencies]
volga = { version = "..." }
tokio = { version = "...", features = ["full"] }
futures-util = "0.3"
use futures_util::StreamExt;
use volga::{App, http::HttpBodyStream, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_post("/count", |mut stream: HttpBodyStream| async move {
let mut total = 0;
while let Some(chunk) = stream.next().await {
total += chunk?.len();
}
ok!(format!("received {total} bytes"))
});
app.run().await
}
Такой подход подходит для тел, которые не хочется держать в памяти целиком: подсчёт хеша загрузки, пересылка чанков в хранилище, разбор построчного потока.
Ограничение размера тела
У каждого тела запроса есть ограничение — 5 МБ, если не задано другое. На тело больше него Волга отвечает 413 Content Too Large, каким бы способом обработчик его ни читал: Json<T>, Form<T>, File, Multipart, HttpBody или поток.
Начиная с 0.14.0 и сообщение одно и то же, каким бы ни был способ чтения, — request body is larger than the limit, — и в таком виде оно доходит до map_err() и клиента. Json<T> и Form<T> больше не ставят перед ним JSON parsing error или Form Data parsing error: тело, отклонённое из-за размера, часто ещё до того, как прочитан хоть байт, никто не разбирал. То же относится к любой другой ошибке чтения тела, а тело, которое не десериализуется, по-прежнему даёт ошибку разбора. Самого ограничения в сообщении нет; его сообщает HttpRequest::body_limit() — для обработчика или map_err(), которые хотят сказать о нём клиенту.
Ограничение для всего приложения задаётся через with_body_limit() или ключом body_limit_bytes секции [server] в файле конфигурации:
use volga::{App, Limit};
#[tokio::main]
async fn main() -> std::io::Result<()> {
// 1 МБ на любое тело запроса
let app = App::new().with_body_limit(Limit::Limited(1024 * 1024));
app.run().await
}
Для группы и для маршрута
Начиная с 0.13.1 группа маршрутов и отдельный маршрут могут задать собственное ограничение через RouteGroup::with_body_limit() и Route::with_body_limit(). Оно заменяет ограничение приложения, а значит, может как уменьшить его, так и увеличить — API держит жёсткий лимит для JSON-эндпоинтов и всё равно принимает файлы на том единственном маршруте, которому они нужны:
use futures_util::StreamExt;
use volga::{App, Json, Limit, http::HttpBodyStream, ok};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.group("/api", |api| {
// Всё, что нужно JSON API
api.with_body_limit(Limit::Limited(64 * 1024));
api.map_post("/messages", |Json(text): Json<String>| async move {
ok!(text)
});
// Единственный маршрут, принимающий файл
api.map_post("/attachments", |mut stream: HttpBodyStream| async move {
let mut total = 0;
while let Some(chunk) = stream.next().await {
total += chunk?.len();
}
ok!(format!("stored {total} bytes"))
})
.with_body_limit(Limit::Limited(20 * 1024 * 1024));
});
app.run().await
}
Какое ограничение получит запрос, решается тем, куда он был смаршрутизирован:
- Побеждает самое конкретное ограничение. Собственное ограничение маршрута — над ограничением его группы, вложенной группы — над внешней, группы — над ограничением приложения. Где никто его не задал, действует ограничение приложения.
- Ограничение группы распространяется на каждый зарегистрированный ею маршрут, включая маршруты вложенных групп, независимо от порядка внутри замыкания — как и остальные настройки группы. Маршрут или вложенная группа, задавшие собственное ограничение, сохраняют его.
- Fallback группы получает ограничение группы. Запрос, который не забрал ни один маршрут или группа, получает ограничение приложения.
Limit::Default— это значение фреймворка по умолчанию, 5 МБ, а не ограничение окружающей группы или приложения. Чтобы унаследовать его, просто не задавайте ограничение.
HttpRequest::body_limit() возвращает ограничение, которое получил запрос, в байтах, или None, если его нет, — в обработчике или в middleware.
Снятие ограничения
without_body_limit() снимает ограничение — для всего приложения или, начиная с 0.13.1, для одной группы или одного маршрута. Limit::Unlimited делает то же самое. Это нужно обработчику, который читает тело потоком и сам следит за его размером, например прокси, пробрасывающему тело дальше:
use volga::{App, HttpBody, HttpResponse};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Сколько принять, решает вышестоящий сервис
app.map_post("/proxy", |body: HttpBody| async move {
HttpResponse::builder()
.status(200)
.body(body)
})
.without_body_limit();
app.run().await
}
Примечание
Без ограничения сколько прочитает сервер, решает клиент. Снимайте его только на маршруте, который сам считает байты или передаёт их тому, кто считает, — и никогда на маршруте, собирающем тело в память.
Когда отправляется 413
Ограничение проверяется по мере чтения тела, поэтому обработчик, который тело не читает, оно не затрагивает. Если же читает:
- Заявленная длина больше ограничения отклоняется до того, как прочитан хоть байт. Если
Content-Lengthуже говорит, что тело не поместится, первое же чтение завершается ошибкой413— даже у обработчика, которому нужны были лишь первые байты. Клиент, отправившийExpect: 100-continue, получает413вместо100 Continueи тело не отправляет вовсе. - Тело без заявленной длины (chunked) отклоняется, как только пришло больше, чем помещается.
- Отклонённое тело даёт одну ошибку и заканчивается. Чтение, превысившее ограничение, возвращает ошибку
413, а каждое следующее — конец тела, поэтому цикл, который логирует ошибку и читает дальше, не зацикливается и не вытягивает остаток загрузки. Верните ошибку через?, и она дойдёт до обработчика ошибок, как любая другая, со статусом413.
Совет
В HTTP/1 соединение нельзя переиспользовать, пока часть тела не прочитана, поэтому после такого ответа оно закрывается. Пока клиент может ещё отправлять данные, Волга продолжает читать и отбрасывать то, что приходит, — не дольше 2 секунд и не дольше 500 мс, если ничего не приходит, — чтобы клиент получил 413, а не сброс соединения. Плавное завершение дожидается и такого соединения. HTTP/2 завершает только поток и сохраняет соединение.
Создание тела
Тот же тип используется и для построения тел ответа — именно это делают макросы семейства ok! под капотом:
| Конструктор | Что создаёт |
|---|---|
HttpBody::full | готовое тело в памяти из всего, что преобразуется в Bytes |
HttpBody::empty | пустое тело |
HttpBody::json / form | сериализованное тело JSON или формы |
HttpBody::file | потоковое тело поверх открытого файла |
HttpBody::stream / stream_bytes | потоковое тело поверх любого Stream |
Примеры
Проброс сырого тела можно посмотреть в примере с multipart.