Вызов внешнего сервиса из кода - таймауты, повторы, журнал
Ходим из кода сайта во внешний сервис так, чтобы его недоступность не роняла оформление заказа и не подвешивала страницу на минуту.
Решение
Отправляем запрос штатным клиентом:
use Bitrix\Main\Web\HttpClient;use Bitrix\Main\Web\Json; // разбор ответа средствами ядра, а не своим кодом
$http = new HttpClient([ 'socketTimeout' => 5, // сколько ждём соединения 'streamTimeout' => 10, // сколько ждём ответа целиком 'waitResponse' => true,]);$http->setHeader('Content-Type', 'application/json');$body = $http->post('https://api.example.com/orders', Json::encode($payload));Оба таймаута здесь задают явно и осознанно. Значения по умолчанию рассчитаны на дружелюбную сеть, а зависший чужой сервис без них держит процесс PHP до предела времени выполнения и занимает место в пуле.
Разбираем ответ полностью:
$status = $http->getStatus();if ($status !== 200) { // тело при ошибке тоже читают: там обычно объяснение отказа $error = $http->getResult();}$data = Json::decode($body ?: '{}'); // разбор может выбросить исключениеПроверяют именно код ответа, а не одно только тело. Пустое тело при коде отказа означает одно, а осмысленный текст ошибки - совсем другое, и разбор без проверки кода превращает отказ в непонятное исключение разбора.
Выносим вызов из запроса посетителя:
// обработчик события кладёт задание, а не ходит в сетьCAgent::AddAgent('\\Vendor\\Sync::pushOrders();', 'main', 'N', 60);// оформление заказа не должно зависеть от чужой доступностиВнешний вызов внутри оформления заказа делает чужой сбой своим. Задание, поставленное в очередь, отправляет данные позже и переживает недоступность сервиса без потери заказа.
Пишем в журнал и повторяем с задержкой:
\Bitrix\Main\Diag\Debug::writeToFile( ['status' => $status, 'body' => mb_substr((string)$body, 0, 500)], date('H:i:s'), 'api.log');// повтор делают с растущей паузой: 1, 5, 30 минут// бесконечные повторы без паузы кладут и чужой сервис, и свой сайт// число попыток ограничивают: после него задание помечают неудачнымЗапись ответа остаётся единственным способом доказать, чья сторона сломалась. Повторы делают с растущей паузой: частый повтор в цикле добивает сервис, который и так отвечает с трудом.
Ключи доступа держат в настройках модуля или в переменных окружения. Ключ в коде уезжает в систему контроля версий и остаётся там навсегда, даже после смены.
Ответ внешнего сервиса стоит проверять на состав, а не только на код. Пришедший успешный код с пустым или урезанным телом ломает разбор ровно так же, как отказ, и замечают это обычно гораздо позже.
Чужой сервис стоит считать ненадёжным по умолчанию. Отказ, таймаут и неожиданный формат ответа - это не исключительные ситуации, а обычный режим работы любой внешней интеграции.
Типичные проблемы
Страница висит по минуте и отдаёт ошибку.
Вызов внешнего сервиса идёт прямо в запросе посетителя без таймаутов. Зависший чужой ответ держит процесс PHP до предела времени выполнения.
Заказы теряются при недоступности сервиса.
Отправка данных встроена прямо в оформление заказа. Её выносят в отдельное задание, которое переживёт недоступность сервиса.
В логе исключение разбора ответа.
Тело ответа разбирается без проверки его кода. При отказе там приходит текст ошибки, а вовсе не ожидаемый формат.
Внешний сервис заблокировал сайт.
Повторы идут без пауз и без ограничения числа попыток. Частые повторы выглядят с чужой стороны как нападение на сервис.
Непонятно, чья сторона виновата.
Запросы и ответы сервиса никуда не записываются. Без журнала разбор чужого сбоя превращается в спор без доказательств.
Частые вопросы
Какие таймауты ставить?
Секунды на соединение и десяток секунд на ответ. Точные значения берут из договорённостей с сервисом, а не наугад.
Сколько раз повторять запрос?
Три-пять попыток с растущей паузой, дальше - в ручной разбор. Бесконечные повторы вредят обеим сторонам.
Где хранить ключ доступа?
В настройках модуля или переменных окружения. Ключ в коде попадает в историю изменений и живёт там вечно.
Нужно ли шифровать передачу?
Всегда, если уходят данные покупателей. Обмен по незащищённому протоколу с персональными данными недопустим.
Смежное
- REST и вебхуки - оглавление подтемы
- Запрос к внешнему сервису не проходит: разбор причин - если вызов вообще не проходит
- Очередь сообщений: фоновая обработка задач в ядре - куда выносят медленный вызов
- Свой провайдер SMS: класс, регистрация, отправка и статусы - тот же вызов внутри провайдера сообщений
- Вебхуки и вызовы REST: настройка, права, разбор ошибок - обратное направление вызовов
- Приём вебхука от внешнего сервиса: точка входа, подпись, повторы - когда вызывают уже вас
- Уведомления команде в мессенджер: вебхук, шаблон, ошибки - типовой пример такого вызова
- Внешняя база данных: второе соединение, запросы, ORM - когда у чужой системы есть только база
- Свой агент и задание по расписанию: создание, шаг, защита - куда выносят отправку
- Обработчик события: регистрация, аргументы, отмена действия - откуда запускают отправку
- Загрузка по ссылке от посетителя: защита от запросов во внутреннюю сеть - когда адрес запроса приходит от посетителя
- Обмен с 1С и HTTP - устройство интеграций целиком
- Свой модуль: структура, установка, обновления - как упаковать код проекта
- Своя таблица на ORM: сущность, запросы, изменение структуры - хранилище под свои данные
- Свои события в своём коде: как дать другим точку расширения - события вместо прямых вызовов
- Свой обработчик оплаты: форма, уведомление, отметка платежа - приём уведомлений от чужой системы
- Своя служба доставки с внешним API: регистрация, расчёт, статусы - чужое API внутри оформления
- Своё API для приложения: контроллер, токен, версии - обратное направление вызова
- Очередь обмена со сторонней системой: задания, повторы, сверка - когда вызов нельзя терять