Как сообщить клиенту о недоступности внешнего источника данных
Что проверяют
Под «клиентом» здесь обычно понимают потребителя вашего API. Проверяют, отличаете ли вы ошибку внешней зависимости от ошибки своего сервиса и передаёте ли её так, чтобы клиент понял, повторять запрос или нет. Следом часто просят показать, где в коде ошибка превращается в HTTP-ответ.
Как отвечать
Каркас из трёх шагов: обнаружить, классифицировать, ответить.
- обнаружить: у
http.Clientпо умолчанию таймаута нет, поэтому задайтеTimeoutили передавайте контекст сcontext.WithTimeoutвhttp.NewRequestWithContext. Иначе недоступный источник держит горутину и соединение сколько угодно; - классифицировать: оборачивайте ошибку через
fmt.Errorfс%wи заведите доменнуюErrUpstreamUnavailable. В хендлере проверяйтеerrors.Is(err, context.DeadlineExceeded)иerrors.Is(err, ErrUpstreamUnavailable); - ответить:
503 Service Unavailableс заголовкомRetry-After, если источник лежит,504 Gateway Timeout, если не дождались,502 Bad Gateway, если он вернул некорректный ответ. В gRPC этоcodes.Unavailableиcodes.DeadlineExceeded; - тело ошибки: машиночитаемый код и понятный текст, удобно в формате Problem Details (RFC 9457,
application/problem+json). Внутренние адреса и стектрейсы наружу не отдаются; - деградация: если есть кэш, отдайте последние данные с пометкой об их возрасте или частичный ответ. Часто это лучше ошибки;
- защита: circuit breaker, например
sony/gobreaker, перестаёт долбить лежащий источник и отвечает сразу, а не через таймаут.
Параллельно ошибка пишется в лог через log/slog с идентификатором запроса и уходит в метрики, чтобы вы узнали о проблеме раньше клиента.
Где спотыкаются
Отдают 500 Internal Server Error на всё подряд. Клиент не отличит баг в вашем коде от временной недоступности партнёра и не поймёт, есть ли смысл повторять.
Забывают про таймауты и контекст. Запросы к лежащему источнику копятся, число горутин и соединений растёт, и отказ партнёра превращается в отказ вашего сервиса.
Пробрасывают наружу исходный текст ошибки вроде dial tcp 10.0.3.14:443: connect: connection refused. Это утечка внутренней топологии, а клиенту такая строка ничего не говорит.