Gzip-ответ занял слишком много памяти в Go: где ставить лимит чтения

Небольшой сжатый ответ после распаковки оказался огромным. Ограничение только размера входного архива не гарантирует ограничение результата. При работе с недоверенным gzip нужно отдельно контролировать объём распакованных данных и время обработки.

Как разобрать ситуацию

В примере gzip.Reader обёрнут io.LimitReader с пределом limit+1. Дополнительный байт позволяет отличить допустимое содержимое ровно на границе от превышения. Если прочитано больше limit, результат отклоняется, а не передаётся дальше как незаметно обрезанный документ. Закрытие reader и исходного тела остаётся обязанностью вызывающего кода.

Пример и ожидаемый результат

Учебная программа сжимает 100 одинаковых символов и пробует получить результат при допустимом размере 10. Она читает максимум 11 распакованных байт и обнаруживает превышение. Внешний URL не вызывается: источник данных создаётся прямо в памяти.

package main

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"strings"
)

func main() {
	var b bytes.Buffer
	w := gzip.NewWriter(&b)
	if _, e := w.Write([]byte(strings.Repeat("x", 100))); e != nil {
		panic(e)
	}
	if e := w.Close(); e != nil {
		panic(e)
	}
	r, e := gzip.NewReader(&b)
	if e != nil {
		panic(e)
	}
	defer r.Close()
	out, e := io.ReadAll(io.LimitReader(r, 11))
	if e != nil {
		panic(e)
	}
	fmt.Println(len(out), len(out) > 10)
}

Программа выводит 11 true. Это означает, что превышение найдено ограниченным чтением; весь распакованный документ не был принят приложением.

Проверка перед применением

  • Проверьте размеры limit−1, limit и limit+1 на распакованном содержимом.
  • Ограничьте также время и размер сжатого входа: один порог не покрывает все виды нагрузки.
  • Не возвращайте усечённый JSON или документ как успешный полный результат.

Если содержимое принято в пределах лимита, всё равно проверяйте его тип и полноту по контракту следующего шага. Лимит отвечает на вопрос ресурса, а не подлинности или корректности данных. Такой тест полезно держать рядом с обычным небольшим корректным ответом, чтобы защита не ломала нормальный рабочий сценарий.

Границы решения

Пример не является полноценной защитой любого архивного формата и не проверяет HTTP-клиент. При автоматической распаковке Transport уточните, на каком уровне вы фактически читаете байты. Память и CPU нужно контролировать вместе.

Если базовый Go уже знаком и хочется связать HTTP, хранение данных и обработку отказов в одну систему, посмотрите наш платный курс

Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker. Перед записью сравните программу со своей задачей и подготовкой.

Техническая справка: pkg.go.dev: документация, pkg.go.dev: документация.

Следующий шаг: Go backend: HTTP, конкурентность и устойчивость сервиса. По смежным вопросам: Отмена HTTP-запроса в Go: как убедиться, что context дошёл до клиента, HTTP retry в Go: почему один список кодов не делает повтор безопасным.