Глава 16 / 51
Том 3 · Разработка протоколов

Тела запросов: инкрементальное чтение фиксированных и chunked

Тело запроса — не кусок памяти, ожидающий вас, а поток. Глава разбирает автомат состояний xrtHttp1BodyRead — и выполняет обязательство /echo из главы 4.

Обзор

Позиционирование: вход POST/PUT-бизнеса. После главы вы безопасно читаете тело в любой кодировке и знаете, где стоят линии обороны против трёх видов враждебного ввода: чрезмерного, битого и медленного.

16.1 Введение

Тела приходят тремя способами: фиксированная длина (Content-Length), chunked и «конец по закрытию соединения» (без длины; редко и опасно). Все делят один интерфейс чтения — автомат pReq->body плюс xrtHttp1BodyRead. Кодировка вас не волнует: автомат сводит три режима к вопросу «сколько прочитано и есть ли ещё».

16.2 Автомат кадрирования

xhttp1body · автомат кадрирования (xsbase.h) режимы: NONE / FIXED (фиксированная длина) / CHUNKED / CLOSE / TUNNEL Read(pBody, buf, cap) │ ├── > 0 байты этого раунда (может быть меньше запрошенного) ├── 0 новых данных нет (повторить позже / ждать событий) └── код ошибки битая строка chunk / конфликтные объявления длины и т.п. готово когда: накоплено == объявленной длине (FIXED) или пришёл терминальный chunk (CHUNKED) трейлеры: до 8 полей в конце chunked, извлекаются автоматом

Ключевое понимание: разборщик HTTP/1.1 отвергает конфликтные объявления длины (Content-Length вместе с Transfer-Encoding — ошибка протокола) — одно из семейств среди 35 мелкогранулярных кодов ошибок; битый ввод останавливается до вашего бизнеса.

16.3 Паттерн цикла чтения

Полный цикл чтения (паттерн демо)проверено на реальной машинеисточник: demo-single/modules/http.h (фрагмент паттерна)
/* Полностью вычитать тело в динамический буфер (маленькие тела).
 * Большие тела обрабатываются по мере поступления — см. вызов. */
static bool ReadBodyAll(XS_HttpReq* pReq, xbuffer** ppOut)
{
    xbuffer* pBuf = xrtBufferCreate(...);
    char arrChunk[8192];

    for ( ;; ) {
        xhttp1bodystatus st = xrtHttp1BodyRead(
            pReq->body, arrChunk, sizeof(arrChunk), &iGot);
        if ( st == XHTTP1_BODY_ERROR ) {
            xrtBufferDestroy(pBuf);
            return false;          /* битый ввод */
        }
        if ( iGot > 0 ) xrtBufferAppend(pBuf, arrChunk, iGot);
        if ( st == XHTTP1_BODY_DONE ) break;
        if ( iGot == 0 ) {
            /* в этом раунде ничего нового: реальный код входит в колбэк по событиям */
        }
    }
    *ppOut = pBuf;
    return true;
}

Что только что произошло: чтение инкрементальное — каждый вызов даёт «что есть сейчас»; если данные ещё не дошли, ждём событий (колбэк повторно входит в последующих циклах драйвера того же соединения). Именно поэтому большая загрузка не тащит всё тело в память: пришёл кусок — обработали кусок.

Теперь можно выполнить /echo из главы 4: POST → полностью вычитать тело → вернуть как есть через ReplyRaw главы 15 (с эхом Content-Type запроса).

16.4 Зонды целостности и 413

  • body_limit (ручка Custom из главы 9): превышение — 413; линия обороны в драйвере, ваш колбэк вообще не вызывается
  • Зонд целостности: драйвер даёт проверку, не потребляющую транспортный буфер, — пришло ли тело целиком, чтобы не отвечать на половине
  • Битый chunked: плохие строки chunk, нелегальные трейлеры, сверхдлинные строки chunk — коды ошибок разборщика; соединение закрывается с 400
  • Медленная атака: idle_timeout — временная линия; соединение, кормящее тело по байту, закрывается по истечении

16.5 Контракт

  • Чтение: тело потребляется только через BodyRead; драйвер сверяет ваш расход с сетью — расхождение рвёт соединение.
  • Завершение: перед XS_OK тело либо дочитано, либо явно возвращено драйверу на вычитку — XS_OK при получтении ломает keep-alive.
  • Память: большие тела обрабатывайте по мере чтения, не берите ReadBodyAll по умолчанию.

Частые ошибки

XS_OK при недочитанном теле
Симптом
На keep-alive следующий запрос смещается / драйвер рвёт соединение.
Причина
У тела остался непотреблённый остаток — драйвер вычитает его за вас, но семантика ответа уже неполна (в части сценариев сразу провал). Дочитайте или явно откажитесь (с записью в лог) до возврата.
«В этом раунде 0» принято за «готово»
Симптом
Большие тела иногда обрабатываются наполовину.
Причина
0 значит лишь «сейчас новых данных нет»; сигнал завершения — DONE автомата (или совпадение накопленной длины). Условие цикла обязано их различать.

Упражнения

БазовыйВыполнение /echo

Реализуйте /echo из главы 4: тело POST возвращается как есть.

Приёмка: проверка на 50 байт и 50 КБ; Content-Type эхом.
Продвинутыйchunked-ввод клиента

Отправьте chunked-тело через curl -H "Transfer-Encoding: chunked" --data-binary и убедитесь, что цикл чтения не требует правок.

Приёмка: обе кодировки дают одинаковый результат.
ВызовПоточное хэширование

Реализуйте /hash: считать SHA-256 по мере чтения (семейство xrtSha), по завершении вернуть hex-дайджест — не кэшируя тело целиком.

Приёмка: память постоянна при загрузке 100 МБ; результат совпадает с sha256sum.

Итоги

ТезисВывод
Единый автоматфиксированные / chunked / close — один интерфейс Read
Инкрементальностькусок за куском; 0 = сейчас нет данных ≠ готово
Оборонаbody_limit 413 · битое 400 · idle_timeout против медленных
Сверка потребленияперед XS_OK тело дочитано или явно возвращено
Что дальше по xrt