> Как сообщить клиенту о недоступности внешнего источника данных (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Wildberries

Стек: Go

> Пример ответа

Короткий ответ

Клиенту нужно сообщить о недоступности внешнего источника данных через явный HTTP-статус (503 Service Unavailable) с телом ответа, содержащим machine-readable код ошибки и человекочитаемое сообщение. Важно не раскрывать внутренние детали инфраструктуры, но дать достаточно информации для диагностики. Используйте структурированный формат ошибки (например, RFC 7807 problem+json) и согласованный контракт ошибок во всём API. Таймауты и retry-логика должны быть настроены на стороне сервера, чтобы клиент получал ответ в разумное время, а не бесконечное ожидание.

Подробное объяснение

Недоступность внешнего источника данных - это временное или постоянное состояние, которое клиент должен отличать от других ошибок. Основные принципы:

  • Статус-код: 503 Service Unavailable - семантически корректный выбор. Он сигнализирует, что сервер временно не может обработать запрос из-за перегрузки или недоступности зависимого сервиса. 500 Internal Server Error использовать не стоит - это скрывает причину и усложняет мониторинг.
  • Заголовок Retry-After: обязателен для 503. Указывает клиенту, когда можно повторить запрос. Это снижает нагрузку на ваш сервис и помогает клиенту строить корректную retry-политику.
  • Тело ответа: должно содержать:
    • code - стабильный строковый идентификатор ошибки (например, external_dependency_unavailable)
    • message - краткое описание для разработчика
    • retry_after - дублирование заголовка в теле для клиентов, которые не читают заголовки
    • details - опционально, но без внутренних IP, hostname и стектрейсов
  • Контракт: ошибка должна быть частью OpenAPI-спецификации. Клиент заранее знает, какие поля ожидать.
  • Идемпотентность: если запрос мог быть частично обработан до падения внешнего источника, клиент должен иметь возможность безопасно повторить. Для этого используйте idempotency keys или явно документируйте поведение.
  • Graceful degradation: если внешний источник нужен не для всех операций, рассмотрите fallback-механизмы (кэш, заглушки) и возвращайте 200 с пометкой degraded: true в теле. Но это отдельный архитектурный trade-off.

На практике

В Go-сервисе типичный сценарий:

  1. HTTP-хендлер вызывает клиент внешнего API через интерфейс с контекстом и таймаутом.
  2. При ошибке соединения, таймауте или 5xx от внешнего сервиса - логируем детали (для себя) и возвращаем 503 клиенту.
  3. Используем middleware для единообразного форматирования ошибок.
  4. Настраиваем мониторинг: метрика external_dependency_failures_total с labels по имени источника и типу ошибки.
  5. Для критичных источников - circuit breaker (например, sony/gobreaker), чтобы не долбить недоступный сервис.

Важно: не пробрасывайте ошибку внешнего сервиса как есть. Клиент не должен знать, что у вас внутри - это и безопасность, и стабильность контракта.

Пример кода

GO
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
RetryAfter int `json:"retry_after,omitempty"`
}
func handleDependencyError(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("external dependency failed: %v", err)
retryAfter := 5 // секунды
w.Header().Set("Content-Type", "application/problem+json")
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(ErrorResponse{
Code: "external_dependency_unavailable",
Message: "внешний источник данных временно недоступен, повторите запрос позже",
RetryAfter: retryAfter,
})
}
// использование в хендлере
func GetUserHandler(deps *Dependencies) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
user, err := deps.UserService.Fetch(ctx, userID)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || isDependencyError(err) {
handleDependencyError(w, r, err)
return
}
// другие ошибки - 500 или 4xx
handleGenericError(w, r, err)
return
}
writeJSON(w, http.StatusOK, user)
}
}

Как отвечать на собеседовании

Начните с главного: 503 + Retry-After + структурированное тело. Затем объясните, почему это важно: клиент может корректно обработать ошибку, отличить от бага в вашем коде, построить retry-политику. Упомяните, что нельзя возвращать 500 - это маскирует проблему. Добавьте про контракт ошибок в OpenAPI и про то, что детали внешнего источника не должны утекать наружу. Если спросят про retry - скажите, что клиент должен использовать экспоненциальную задержку с джиттером, а сервер - ограничивать количество попыток. Хорошо упомянуть circuit breaker и graceful degradation как дополнительные меры.

Что проверяет интервьюер

  • Понимание семантики HTTP-статусов и умение выбрать правильный.
  • Умение проектировать контракт ошибок, а не просто возвращать текст.
  • Осознание проблем мониторинга и наблюдаемости.
  • Понимание взаимодействия между сервисами: таймауты, retry, backpressure.
  • Уровень senior: способность видеть картину целиком - от клиента до внешнего источника, включая эксплуатационные аспекты.

Типичные ошибки

  • Возврат 500 вместо 503 - клиент не может отличить вашу ошибку от недоступности зависимости.
  • Отсутствие Retry-After - клиент не знает, когда повторять.
  • Проброс внутренних деталей (hostname, IP, стектрейс) в теле ответа - утечка информации.
  • Бесконечное ожидание ответа от внешнего источника без таймаута - клиент висит.
  • Игнорирование идемпотентности - повторный запрос может создать дубликаты данных.
  • Разные форматы ошибок в разных эндпоинтах - усложняет клиентскую обработку.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью