Волга
Главная
Agent Skill
API Docs
GitHub
  • English
  • Русский
Главная
Agent Skill
API Docs
GitHub
  • English
  • Русский
  • Главная
  • Старт

    • Быстрый старт
    • Обработчики
    • Параметры маршрута
    • Параметры запроса
    • Группировка маршрутов
  • Запросы и ответы

    • Заголовки (Headers)
    • Работа с JSON
    • Работа с Form Data
    • Работа с файлами
    • Multipart-ответы
    • Сырое тело запроса
    • Cookies
    • Валидация запросов
  • Middleware и инфраструктура

    • Основы Middleware
    • Пользовательские Middleware
    • Параметризованные Middleware
    • Сжатие ответов
    • Распаковка запросов
    • CORS (Cross-Origin Resource Sharing)
    • Статические файлы
    • Rate Limiting
    • Файлы конфигурации
    • OpenAPI
  • Безопасность и доступ

    • Аутентификация и авторизация
    • OAuth 2.1 и OpenID Connect
    • OAuth 2.1 Клиент
    • Гранты «сервис-сервис»
    • DPoP — токены, привязанные к отправителю
  • Надежность и наблюдаемость

    • Центральный обработчик ошибок
    • Логгирование и Трассировка
    • Отмена запросов
    • Плавное завершение работы
  • Протоколы и realtime

    • Версии и методы HTTP
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Продвинутые паттерны

    • Внедрение Зависимостей
    • Пользовательская обработка методов HEAD, OPTIONS и TRACE
  • Agent Skill

Сырое тело запроса

Большинство обработчиков используют типизированные экстракторы — 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.

Последнее обновление: 20.10.2026, 15:26
Prev
Multipart-ответы
Next
Cookies