Retry-After в Go: секунды, HTTP-дата и оставшееся время
Зависимый API отвечает 429 и присылает Retry-After. В тесте стоит значение 5, поэтому клиент вызывает strconv.Atoi и ждёт пять секунд. У другого поставщика заголовок содержит HTTP-дату, и тот же клиент немедленно повторяет запрос, приняв ошибку разбора за нулевую задержку. Ограничение сервера фактически игнорируется.
Две формы одного заголовка
У Retry-After есть две формы: неотрицательное целое число секунд и HTTP-дата. Для второй формы в Go подходит http.ParseTime. Дату нужно сравнить с выбранным now, получить длительность и обработать уже прошедшее значение. Передавать now параметром полезно для воспроизводимых тестов: проверка не зависит от часов машины в момент запуска.
Даже корректная задержка может не помещаться в оставшееся время запроса. Бессмысленно ждать пять секунд, если у всей операции осталось две. В примере парсер принимает ещё budget и возвращает ошибку, когда пауза превышает его. Это явное решение клиента прекратить работу, а не обещание выполнить retry любой ценой.
Здесь проверяется только интерпретация указания сервера. Разрешение повторить меняющую операцию определяется её идемпотентностью и известностью исхода предыдущей попытки. Заголовок не превращает POST в безопасную операцию. Эту границу полезно держать отдельно от разбора синтаксиса, чтобы один рабочий парсер не стал случайной универсальной политикой повторов.
Разобрать задержку без переполнения
package main
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
func retryDelay(raw string, now time.Time, budget time.Duration) (time.Duration, error) {
raw = strings.TrimSpace(raw)
if raw == "" || budget < 0 {
return 0, fmt.Errorf("missing header or expired budget")
}
numeric := true
for _, c := range raw {
if c < '0' || c > '9' {
numeric = false
}
}
var delay time.Duration
if numeric {
seconds, err := strconv.ParseUint(raw, 10, 64)
if err != nil || seconds > uint64(budget/time.Second) {
return 0, fmt.Errorf("delay exceeds budget")
}
delay = time.Duration(seconds) * time.Second
} else {
moment, err := http.ParseTime(raw)
if err != nil {
return 0, fmt.Errorf("invalid Retry-After: %w", err)
}
delay = moment.Sub(now)
if delay < 0 {
delay = 0
}
}
if delay > budget {
return 0, fmt.Errorf("delay exceeds budget")
}
return delay, nil
}
func main() {
now := time.Date(2026, 1, 2, 7, 0, 0, 0, time.UTC)
for _, raw := range []string{"5", now.Add(5 * time.Second).Format(http.TimeFormat)} {
delay, err := retryDelay(raw, now, 8*time.Second)
if err != nil || delay != 5*time.Second {
panic("wrong delay")
}
fmt.Println(delay)
}
for _, raw := range []string{"9", "-1", "oops", "9999999999999999999999999"} {
if _, err := retryDelay(raw, now, 8*time.Second); err == nil {
panic("invalid or excessive delay accepted")
}
}
}
Числовая ветка проверяет верхнюю границу до преобразования в Duration и умножения. Иначе огромное число секунд может переполнить целочисленное представление и превратиться в короткое или отрицательное ожидание. Знак плюс и дробные секунды не входят в выбранный числовой формат: такой вход попадёт в разбор даты и будет отвергнут.
Ожидать в бюджете операции
Для HTTP-даты точность и корректность часов имеют значение. Если машины сильно расходятся по времени, вычисленная задержка будет неверной. При работе с внешним API полезно наблюдать ответы Date и следить за синхронизацией системных часов. Не компенсируйте произвольное расхождение молчаливой подстановкой нуля: это снова создаст слишком ранние запросы.
После выбора паузы ожидайте её с учётом context.Done, чтобы отмена входящего запроса прервала ожидание. Общий deadline должен включать все попытки и паузы. При этом оставьте время на сам следующий обмен: budget в примере относится только к допустимой задержке, а вызывающий код определяет резерв на работу.
Сохраните тест обеих форм, прошлой даты, пустого значения и чрезмерно большого числа. Для прошлой даты выбран нулевой интервал, но право на немедленный повтор всё ещё решает политика операции. Так синтаксис заголовка и безопасность повтора остаются двумя явно проверяемыми правилами.
Продолжить практику
В курсе микросервисов на Go есть общий бюджет попыток, идемпотентность и защита зависимости от перегрузки. Откройте программу и продолжите от разбора Retry-After к полному сценарию: один ключ операции, ограниченное время и понятный исход после потерянного ответа.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
