Как реализовать middleware для Go клиента, чтобы выполнять дополнительные действия перед запросом
Что проверяют
Знаете ли вы, что в net/http клиентское middleware строится на http.RoundTripper, и помните ли его контракт. Следом обычно просят добавить ретраи и спрашивают, что станет с телом запроса при повторе.
Как отвечать
Точка расширения клиента это поле Transport у http.Client. Middleware оборачивает транспорт:
- адаптер: в стандартной библиотеке нет
http.RoundTripperFunc, его объявляют сами:type RoundTripperFunc func(*http.Request) (*http.Response, error)плюс методRoundTrip, который вызывает функцию; - сигнатура:
func(next http.RoundTripper) http.RoundTripper, внутри действия до запроса, вызовnext.RoundTrip(req)и действия после; - цепочка: функция
Chain(base, mws...)применяет обёртки в обратном порядке, чтобы первая в списке срабатывала первой. Еслиbaseравенnil, беритеhttp.DefaultTransport.
Главное здесь контракт RoundTripper из документации. Он не должен менять входящий запрос, кроме чтения и закрытия тела, поэтому для заголовка авторизации или ID трассировки делаете r := req.Clone(req.Context()) и меняете копию. И он не должен интерпретировать ответ: статус 500 это валидный ответ, а не ошибка транспорта.
Для ретраев тело запроса нужно перечитать, а io.Reader читается один раз. Выручает req.GetBody: http.NewRequest заполняет его сам для bytes.Buffer, bytes.Reader и strings.Reader. Тело неудачного ответа перед повтором дочитайте и закройте, иначе соединение не вернётся в пул. Паузы между попытками делайте через select с req.Context().Done(), чтобы отмена срабатывала сразу.
Для тестов подменяют next заглушкой или поднимают httptest.NewServer и смотрят, какие заголовки доехали.
Где спотыкаются
Мутируют исходный req, например вызывают req.Header.Set прямо в middleware. При повторе это даёт гонки и задвоенные заголовки.
Создают новый http.Client или http.Transport на каждый запрос. Пул соединений теряется, и под нагрузкой кончаются сокеты.
Путают клиентское и серверное middleware и пишут обёртку над http.Handler. Она обрабатывает входящие запросы вашего сервера, а к исходящим вызовам клиента отношения не имеет.