Перейти к основному содержимому

Интеграция с внешним HTTP API

Исходящий HTTP-вызов выполняется из action через зарегистрированный клиент. Транспорт и middleware настраиваются в provider, а business mapping и реакция на ошибку принадлежат сценарию.

1. Проверить контракт и выбрать клиент​

Найдите upstream OpenAPI и нужную операцию либо исходный Laravel-вызов. Запишите метод, path/query/body, URL/API prefix, auth, headers, response, pagination и ошибки. Для изменяющего вызова определите безопасность повтора и возможное состояние после timeout.

Для Ensi upstream используйте опубликованный generated *-client-go, установленный в согласованной версии. Создание копии его API в app/adapters/clients или ручная правка generated-кода приведёт к расхождению с контрактом. Отсутствующая нужная операция требует обновления источника генерации и клиента.

Сначала добавьте выбранную версию модуля через elc go get, как описано в работе с пакетами. Затем из корня потребителя установите пакет:

elc ./artisan package:install gitlab.com/greensight/ensi/catalog/clients/pim-client-go

Просмотрите изменения installer-а в app/providers, app/facades, config/ensi_clients, config/app.go и bootstrap. Проверьте регистрацию ровно одного provider и сохранность собственного bootstrap. При обновлении зависимости опубликованный локальный provider/config может требовать отдельного изменения.

Для API вне Ensi переиспользуйте поддерживаемый SDK либо HTTP-адаптер сервиса. Собственный адаптер должен иметь контролируемые timeout, сериализацию, ошибки и тестовую границу; один http.Client на каждый handler не нужен.

2. Собрать URL и конфигурацию​

Конфигурация PIM в CMS регистрирует ключ ensi_clients.pim.base_url и добавляет /api/v1 к CATALOG_PIM_SERVICE_HOST. Generated Products.GetProduct использует путь /products/published/{id}. Итоговый запрос должен содержать /api/v1 ровно один раз.

ЗначениеПример для этой версии PIM-клиента
Env hosthttp://catalog-pim — фактический hostname задаётся окружением
Config base URLhttp://catalog-pim/api/v1
Путь операции клиента/products/published/101
Итоговый URLhttp://catalog-pim/api/v1/products/published/101

Другой клиент может формировать prefix иначе. Проверьте его setup/config, published config и generated operation. При добавлении настройки обновите .env.example; секретные значения задаются окружением. Вызов приложения должен получать тот же config, который проверяет тест.

3. Зарегистрировать общий transport и provider​

В CMS несколько клиентов используют функцию clientOptions. Её сокращённый по окружению пример совпадает с используемыми API:

func clientOptions(configKey, service, circuitService string) (
httpclient.Request,
[]ensihttp.Middleware,
) {
baseURL := facades.Config().GetString(configKey)
request := facades.Http().Client().BaseUrl(baseURL)
clientmiddleware.ConfigureCircuitBreaker(request.HttpClient(), circuitService)
middlewares := clientmiddleware.AppendLogging(service, []ensihttp.Middleware{
clientmiddleware.Metrics(baseURL),
})
return request, middlewares
}

httpclient — github.com/goravel/framework/contracts/http/client, ensihttp — gitlab.com/greensight/ensi/packages/goravel-ensi-http-client/http, clientmiddleware — пакет приложения. В этом helper таймаут явно не задаётся: проверьте значение request.HttpClient().Timeout, общий config и требования операции; настройку выполняйте один раз в provider.

Provider регистрирует клиент в DI:

func (*PimServiceProvider) Register(app foundation.Application) {
pim.App = app
app.Singleton(pim.Binding, func(foundation.Application) (any, error) {
request, middlewares := clientOptions(
"ensi_clients.pim.base_url",
pim.Binding,
"catalog_pim",
)
return pim.New(request, middlewares), nil
})
}

pim — alias generated-модуля, foundation — contracts Goravel. Facade facades.Pim() разрешает этот binding. Устанавливая middleware, проверьте, не разделяется ли *http.Client или transport между другими клиентами. Повторное навешивание обёрток на каждый запрос и изменение http.DefaultTransport затрагивают другие вызовы.

Контекст и initial-event​

Передавайте входящий context.Context в каждый вызов клиента. Он несёт отмену, deadline, request profiler и X-Initial-Event, если его установил общий HTTP/console/Kafka bootstrap. Создание нового context.Background() внутри action теряет эту связь.

В текущем общем коде ConfigureCircuitBreaker также подключает initial-event propagation, в том числе при отключённом breaker. Она клонирует HTTP-запрос и headers перед добавлением заголовка. Если transport настраивается другим путём, проверьте наличие этой обвязки.

Singleton не должен хранить context, токен и другие изменяемые данные конкретного запроса. Request-specific fluent-настройки используйте согласно реализации клиента; correlation header не означает автоматическую передачу входящего Authorization.

Метрики, логи и circuit breaker​

  • Metrics(baseURL) учитывает исходящие вызовы и сегмент времени в request profiler.
  • AppendLogging включает файловый лог при HTTP_CLIENT_LOG; тела записываются согласно его текущей реализации.
  • ConfigureCircuitBreaker использует отдельный service key для окна отказов и параметры cb.* из config/cb.go.

В каркасе CMS/OMS для breaker сохранены имена env GANESHA_DISABLE_MIDDLEWARE, GANESHA_TIME_WINDOW, GANESHA_FAILED_RATE_THRESHOLD, GANESHA_MINIMUM_REQUESTS, GANESHA_INTERVAL_TO_HALF_OPEN. Текущий transport считает неуспешными HTTP-ответы от 400, а не только 5xx; открытый breaker возвращает ошибку до вызова upstream. Проверяйте, соответствует ли эта политика интеграции.

Circuit breaker сам по себе не задаёт retry. Согласуйте timeout, deadline и число попыток, чтобы несколько слоёв не повторяли один запрос независимо. Детали наблюдаемости — в метриках и логировании.

4. Вызвать клиент из action и отобразить результат​

Пример чтения продукта через опубликованный PIM API:

func loadProduct(ctx context.Context, productID int) (*pim.ProductResponse, error) {
response, err := facades.Pim().Products.GetProduct(ctx, strconv.Itoa(productID), nil)
if err != nil {
return nil, fmt.Errorf("load product %d: %w", productID, err)
}
if response == nil {
return nil, fmt.Errorf("empty product response for %d", productID)
}
return response, nil
}

fmt, strconv и context — стандартные пакеты. Action решает, как использовать response.Data: проверить разрешённое состояние, сопоставить upstream ID с локальным FK и преобразовать поля в собственный DTO. Не возвращайте весь upstream envelope как data своей операции, если контракт ожидает сущность.

Для batch search явно выберите pagination или пройдите все страницы. Успешный ответ первой страницы не подтверждает существование всего списка ID. Проверяйте возвращённые ID и поведение при частично найденном наборе.

При добавлении обязательных filters/includes не теряйте пользовательские значения и не мутируйте разделяемые maps. Используйте существующие helpers копирования запроса только после проверки их типов и правил приоритета.

5. Определить ошибки и безопасность повторов​

ОтказРешение в сценарии
Upstream 400/404/409 и другие 4xxСопоставить доменному смыслу либо проксировать разрешённый статус, если семантика совпадает
5xx, malformed responseВернуть системную ошибку зависимости, сохранив причину
Timeout/cancelСохранить cause и выбранное внешнее поведение; не представлять результат как успешную пустую сущность
Открытый circuitРаспознать через общий helper и применить принятую политику доступности
Timeout после отправки записиУчесть, что upstream мог уже применить изменение

Для прямого proxy используется commonhttp.ProxyError с явным списком статусов своей OpenAPI. Для запроса, собранного бизнес-логикой, такой же upstream 400 может быть внутренним сбоем; используйте Error или типизированный доменный отказ. Политика описана в HTTP-ответах.

Повтор изменяющего вызова допускается при установленной идемпотентности либо стабильном idempotency key, поддержанном upstream. SQL rollback не отменяет уже выполненную HTTP-запись. Сохраняйте существующую стратегию компенсации/повтора и не добавляйте новый цикл поверх retry клиента без проверки.

6. Проверить интеграцию с контролируемой зависимостью​

В компонентном тесте используйте fake установленного generated-клиента. Например, минимальная подмена чтения PIM по конкретному ID:

pimFake := pim.Fake()
pimFake.GetProductAt(
map[string]string{"id": "101"},
map[string]any{"data": map[string]any{"id": 101}},
).Bind()

Заполните response нужными сценарию полями через фабрику или fixture. Bind() активирует накопленные stub-ы. Затем вызовите собственный endpoint/action и проверьте результат и состояние. Это фрагмент подготовки теста, не доказательство его выполнения.

Helpers AssertGetProduct, AssertSearchProducts проверяют, был ли отправлен соответствующий метод/путь; callback позволяет проверить body, query и headers. Проверяйте возвращаемый bool через assert/require. Сам факт хотя бы одного вызова не доказывает точное количество и порядок: для этого используйте доступный recorder/assertions текущего fake.

СценарийЧто утверждать
УспехВыбранный клиент, method/path, body/query/headers и собственный DTO
Несколько вызововКоличество, порядок и отсутствие лишних вызовов
Ошибка upstreamПубличный статус/ошибка, SQL-состояние и отсутствие последующих эффектов
Nullable/emptyВерное отображение null, пустой коллекции и nullable data
Retry/idempotencyПовторяемые запросы и состояние после неопределённого результата записи

Сброс registry — ensihttpfake.Reset(facades.App().MakeHttp()), где ensihttpfake — goravel-ensi-http-client/fake. Соблюдайте setup/teardown текущего suite, очищайте конфигурацию и разрешённые singleton bindings; refresh приложения может восстановить реальный клиент. Если HTTP-тест встраивает ComponentSuite и переопределяет TearDownTest, вызовите в нём s.ComponentSuite.TearDownTest(): сброс HTTP registry не выполняет проверку Kafka-сообщений. В Customer API Gateway этот общий suite работает без настройки БД. Подробности — в автотестах.

Fake клиента может обходить реальный transport. Для проверки initial-event, timeout или circuit breaker используйте отдельный контролируемый локальный HTTP transport/server. Прохождение fake-теста не подтверждает живую upstream-интеграцию.

Исходные примеры​

Используйте implement-http-client-integration для реализации и test-component-behavior для проверки сценария. Пример узкой проверки потребителя в CMS:

elc go test ./app/modules/products/tests/products --env=.env.testing -count=1
elc mise run check:env
elc go build -o build/app