Что делать при тайм-ауте запроса
Что проверяют
Знаете ли вы, что в Go дедлайн живёт в контексте и пробрасывается по всей цепочке вызовов, и понимаете ли разницу между ограничением на весь вызов и таймаутами отдельных фаз.
Как отвечать
Базовый ответ: таймаут задаётся контекстом и передаётся в запрос.
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
if errors.Is(err, context.DeadlineExceeded) {
// дедлайн истёк
}Дальше разверните, что таймаутов несколько и они про разное:
http.Client.Timeoutограничивает вызов целиком, включая чтение тела;- контекст даёт тот же дедлайн, но его можно передать сквозь всю цепочку и отменить досрочно;
- в
http.Transportотдельно настраиваются установка соединения, рукопожатие TLS и ожидание заголовков ответа; - на сервере нужны
ReadHeaderTimeout,WriteTimeout,IdleTimeout, иначе медленный клиент держит соединение сколько захочет.
Главная мысль: дедлайн один на всю обработку запроса. Обработчик берёт r.Context(), каждый вызов вниз получает производный контекст, и когда клиент отвалился, вся ветка работы сворачивается сама. Сверху обвязка: повтор с джиттером для идемпотентных вызовов, ограничение числа одновременных запросов, предохранитель.
Где спотыкаются
Пишут defer cancel() в функции, которая возвращает *http.Response наружу. Контекст отменяется на выходе, и вызывающий получает ошибку при чтении тела. Либо тело читается до возврата, либо cancel вызывается уже после чтения.
Не закрывают и не дочитывают resp.Body. Соединение не возвращается в пул, при массовых таймаутах кончаются дескрипторы и лимит соединений на хост, и таймаутов становится только больше.
Заводят внутри обработчика свежий context.Background(), чтобы «работа доделалась». Клиент уже ушёл, а горутина продолжает жечь ресурсы. Если фоновая работа правда нужна, ей полагается собственный дедлайн и очередь, а не тихое игнорирование отмены.