отработаны находки ревью кода
Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера, 4 «сейчас», 2 развилки. Все закрыты регрессионными тестами. - схема точки сна определяется по самой точке, а не по индексу в исходном массиве: одна пропущенная точка меняла эпизод и сводку местами - доставка сворачивается одной транзакцией: частичное состояние было недетерминированным (восемь прогонов — семь состояний) - граница размера на распакованном теле: 400 КиБ gzip разворачивались в 400 МиБ мимо max_body_mb - столкновение — расхождение канонических форм, а не байтов; WARN с координатами объекта; payload без HTML-экранирования - выравнивание по местной метке: получасовые зоны уводили часовую выгрузку в minute - доставка из одних суточных сводок больше не отвергается целиком - единицы не переписываются молча; счётчик считает сохранённые точки - все выходы Fold логируются, исход пишется на переживающем отмену контексте Четыре развилки вынесены блокерами в беклог.
This commit is contained in:
@@ -105,6 +105,18 @@ func (a *api) safeHeaders(r *http.Request) map[string][]string {
|
||||
return out
|
||||
}
|
||||
|
||||
// readBody читает тело запроса, при необходимости распаковывая gzip.
|
||||
//
|
||||
// Граница стоит на РАСПАКОВАННОМ потоке, а не только на сжатом. Иначе
|
||||
// `MaxBytesReader` ограничивает то, что приехало по сети, а в память попадает
|
||||
// то, что из этого развернулось: измерено — 400 КиБ сжатого тела дают 400 МиБ,
|
||||
// принимаются со статусом 200 и выедают гигабайт. Потолок степени сжатия gzip
|
||||
// около 1030:1, так что при штатных 64 МиБ речь о десятках гигабайт, и
|
||||
// параллельные запросы складываются.
|
||||
//
|
||||
// Цена отказа здесь наивысшая в проекте: приём — единственное место, где поток
|
||||
// вообще существует. Доставка, не попавшая в архив, не попадает и в журнал —
|
||||
// телефон её не перешлёт.
|
||||
func readBody(w http.ResponseWriter, r *http.Request, maxBody int64) ([]byte, error) {
|
||||
var src io.Reader = http.MaxBytesReader(w, r.Body, maxBody)
|
||||
|
||||
@@ -114,22 +126,30 @@ func readBody(w http.ResponseWriter, r *http.Request, maxBody int64) ([]byte, er
|
||||
return nil, errBadGzip
|
||||
}
|
||||
defer func() { _ = gz.Close() }()
|
||||
src = gz
|
||||
// Лишний байт — способ отличить «ровно по границе» от «больше неё»,
|
||||
// не читая всё до конца.
|
||||
src = io.LimitReader(gz, maxBody+1)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return nil, err //nolint:wrapcheck // разбирается writeReadError по типу
|
||||
}
|
||||
if int64(len(body)) > maxBody {
|
||||
return nil, errTooLargeUnpacked
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
var errBadGzip = errors.New("тело не распаковывается как gzip")
|
||||
var (
|
||||
errBadGzip = errors.New("тело не распаковывается как gzip")
|
||||
errTooLargeUnpacked = errors.New("распакованное тело больше допустимого размера")
|
||||
)
|
||||
|
||||
func writeReadError(w http.ResponseWriter, err error) {
|
||||
var tooLarge *http.MaxBytesError
|
||||
switch {
|
||||
case errors.As(err, &tooLarge):
|
||||
case errors.As(err, &tooLarge), errors.Is(err, errTooLargeUnpacked):
|
||||
writeError(w, http.StatusRequestEntityTooLarge, "пакет больше допустимого размера")
|
||||
case errors.Is(err, errBadGzip):
|
||||
writeError(w, http.StatusBadRequest, "тело не распаковывается как gzip")
|
||||
|
||||
Reference in New Issue
Block a user