HTTP trailer в Go пустой: когда читать итоговый заголовок
Сервис потоково отдаёт файл и хочет передать контрольную сумму, вычисленную по реально отправленным байтам. В начале ответа хеш ещё неизвестен. Разработчик добавляет заголовок после записи тела, но клиент его не видит: обычные заголовки уже ушли по сети. Для некоторых итоговых метаданных HTTP предусматривает trailers.
Передать итоговые метаданные
Перед началом тела сервер объявляет имена будущих полей через Header Trailer. После записи тела он задаёт их значения. Клиент net/http размещает итоговые поля в Response.Trailer. Окончательные значения доступны после чтения Body до EOF. Проверка сразу после Client.Get может увидеть только объявленный ключ без его будущего значения.
В примере сервер вычисляет SHA-256 по тем же байтам, которые пишет клиенту, через MultiWriter. Затем помещает шестнадцатеричный хеш в X-Body-SHA256. Клиент дочитывает тело, вычисляет свою сумму и сравнивает с trailer. Это проверяет весь цикл: объявление, отправку итогового значения и момент его получения.
Используется реальный httptest-сервер, а не только ResponseRecorder: важна именно передача завершающих метаданных через HTTP-транспорт. Имя X-Body-SHA256 выбрано как собственное поле демонстрационного протокола. Если система использует стандартизированный digest-контракт, согласуйте его точное имя, формат и область вычисления отдельно.
Прочитать тело и проверить хеш
package main
import (
"crypto/sha256"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Add("Trailer", "X-Body-SHA256")
digest := sha256.New()
if _, err := io.Copy(io.MultiWriter(w, digest), strings.NewReader("export-data")); err != nil {
return
}
w.Header().Set("X-Body-SHA256", fmt.Sprintf("%x", digest.Sum(nil)))
}))
defer server.Close()
response, err := server.Client().Get(server.URL)
if err != nil {
panic(err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
expected := sha256.Sum256(body)
trailer := response.Trailer.Get("X-Body-SHA256")
if string(body) != "export-data" || trailer != fmt.Sprintf("%x", expected) {
panic("body and trailer mismatch")
}
fmt.Println(string(body), trailer)
}
Не используйте trailer для решения, которое клиент обязан принять до чтения содержимого. Авторизация, выбор маршрута и основной HTTP-статус уже должны быть определены раньше. Если обработка потока завершилась ошибкой после отправки 200, заменить статус нельзя. Контракт может содержать итоговый признак успешности в trailer, но клиент должен специально его проверять и считать отсутствие признаком незавершённого результата.
Определить успешное завершение потока
Посредники и потребители поддерживают trailers неодинаково. Перед использованием проверьте конкретный прокси, CDN и клиентскую библиотеку. Если итоговое поле критично, клиент должен отвергать ответ без него, а не молча принимать файл. Для системы с неподходящими посредниками проще заранее подготовить результат и передать контрольную сумму обычным заголовком либо отдельным документом метаданных.
Хеш в одном и том же соединении проверяет соответствие данных и заявленной суммы, но не подтверждает доверие к отправителю. При угрозе подмены нужен защищённый транспорт и, когда требуется, подпись из независимого доверенного источника. Эти требования отличаются от обнаружения повреждённого или неполного потока.
Не обращайтесь к Trailer конкурентно с чтением Body: транспорт обновляет значения по мере достижения конца. Сначала завершите чтение в одном владельце ответа, затем проверяйте метаданные. Если читатель остановился раньше по лимиту размера, итоговые поля могут не быть доступны; такой ответ уже не соответствует сценарию полной проверки файла.
Добавьте тест обрыва до конца и отсутствующего итогового поля. Полезный результат операции — принятые байты с проверенным завершением, а не только полученные заголовки со статусом 200. Такой тест заставляет потребителя явно выразить момент, когда выгрузку можно считать готовой.
Продолжить практику
В курсе «Микросервисы на Go» есть разбор пустого ответа, обрыва и повреждённых данных, а затем контрактные тесты клиента. Откройте программу и продолжите сценариями отказа: важно определить, что уже получил клиент и какой сигнал позволяет считать операцию завершённой.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
