Таймауты HTTP: connect, request, idle, slowloris

О лекции

Нужны разные таймауты: connect на handshake, чтение заголовков и тела, запись ответа, idle keep-alive. Нулевой таймаут часто значит «ждать вечно». Context отменяет работу, когда клиент ушёл. Slowloris держит слоты капельной подачей заголовков.

Авторизация и устойчивость HTTP http собеседование

Одна цифра «Timeout: 30s» в конфиге ничего не объясняет. Connect timeout ограничивает TCP и TLS handshake. Read/write timeout — сколько ждать данные на уже установленном соединении. Idle — сколько держать keep-alive без трафика, прежде чем закрыть.

На каждом hop свой бюджет: шлюз, сервис, БД. Если таймаут downstream больше, чем у родителя, родитель уже отдал 408/499 клиенту, а дочерний всё ещё пишет в Postgres. Цепочка должна сужаться вниз: внутренний вызов короче внешнего бюджета.

Пример. Шлюз ждёт 2 секунды. Сервис ходит в БД с таймаутом 10 секунд. Клиент уже получил 504. SQL всё ещё работает и держит соединение в пуле. Работа, которую никто не получит.

В Go r.Context() в хендлере отменяется, когда клиент отключился или сработал дедлайн запроса — если сервер это пробросил. Игнорировать контекст в SQL и исходящем HTTP — типичный провал: работа живёт после disconnect.

Сервер в Go

ReadHeaderTimeout критичен. Без него клиент может капать заголовки сколь угодно долго и держать горутину и файловый дескриптор. Это основной щит от slowloris на самом сервере.

ReadTimeout включает тело. WriteTimeout — ответ. С долгим стримингом будьте осторожны: короткий WriteTimeout оборвёт SSE. IdleTimeout закрывает keep-alive. Нуль обычно значит «без ограничения».

srv := &http.Server{
	Addr:              ":8080",
	ReadHeaderTimeout: 5 * time.Second,  // slowloris: капельные заголовки
	ReadTimeout:       15 * time.Second, // заголовки + тело
	WriteTimeout:      30 * time.Second,
	IdleTimeout:       60 * time.Second, // keep-alive без трафика
}
// хендлер:
//   ctx := r.Context() // отмена при disconnect / дедлайне
//   db.QueryRowContext(ctx, q)
// клиент: Timeout всего запроса vs Dial/TLS vs IdleConnTimeout пула
// downstream timeout < parent, иначе родитель уже ошибся, а SQL ещё идёт

Разные поля http.Server — разные фазы. Нуль обычно означает «без ограничения».

Клиент и цепочка

У http.Client поле Timeout — весь запрос целиком. Transport отдельно: DialContext, TLSHandshakeTimeout, ResponseHeaderTimeout, IdleConnTimeout пула. Смешивать «один Timeout на всё» и «infinite transport» опасно: легко забыть одну фазу.

Slowloris

Идея атаки: много соединений, заголовки или тело капают по байту. Воркеры и файловые дескрипторы кончаются дёшево, без высокого RPS.

Пример. Клиент открыл 10 000 соединений. Каждые 20 секунд досылает по одному заголовку. Сервер без ReadHeaderTimeout держит 10 000 горутин. Нормальные клиенты не могут подключиться. Защита: ReadHeaderTimeout, лимит заголовков, лимит соединений на IP, прокси с нормальными таймаутами.

Типичная ошибка

Выставить WriteTimeout короче стрима или SSE и удивляться обрыву. Или поставить downstream таймаут больше родительского «чтобы успеть всегда» — успеете записать в БД после того, как клиент уже ушёл. Нулевой ReadHeaderTimeout оставляет slowloris открытым.

На собеседовании

Назовите connect, ReadHeader, Read/Write, Idle и что будет, если один из них 0. Свяжите r.Context с отменой SQL. Downstream должен быть короче родителя. ReadHeaderTimeout — первая линия против капельных заголовков.