Интеграция Drupal со сторонним API без типичных ошибок
Надёжная интеграция Drupal со сторонним API начинается не с запроса в контроллере, а с контракта, таймаутов, обработки ошибок и отдельного сервиса. Разбираем рабочую схему для Drupal 10.x и 11.x.

Интеграция Drupal со сторонним API часто ломается не на авторизации, а на деталях: запрос выполняется прямо в контроллере, ответ без проверки записывается в базу, а зависший сервис блокирует PHP-процесс. Для Drupal 10.x и 11.x безопаснее вынести HTTP-вызов в отдельный сервис на базе Guzzle, задать таймауты, различать ошибки сети и API, а повторяемые операции отправлять через очередь. Ниже — практическая схема с фрагментами кода и критериями выбора.
Сначала зафиксируйте контракт API
До написания PHP-кода соберите минимальную спецификацию внешнего сервиса. Вам нужны не только URL и токен, но и поведение API в нештатных ситуациях.
- HTTP-метод, конечная точка и обязательные заголовки;
- формат тела запроса и ответа: JSON, form-data или другой вариант;
- коды успешного ответа и структура ошибки;
- таймауты, лимиты запросов и правила повторной отправки;
- идемпотентность операции — можно ли безопасно отправить тот же запрос повторно;
- срок действия токена и способ его обновления.
Проверьте контракт отдельно от Drupal: через официальную коллекцию Postman, curl или тестовый скрипт. Не переносите в код секреты из примеров документации. Для локальной настройки используйте переменные окружения или защищённое хранилище конфигурации.
Правильная интеграция — это не «мы получили 200 OK». Это предсказуемое поведение при таймауте, дубликате, частичном ответе и временной недоступности партнёра.
Технический термин. Идемпотентность — свойство операции, при котором повторение одного и того же запроса не создаёт дополнительный побочный эффект. Например, повторное обновление заказа по уникальному идентификатору может быть идемпотентным, а повторное создание платежа — нет.
Выберите место для HTTP-вызова
В Drupal можно сделать запрос из контроллера, обработчика формы, плагина или cron-задачи. Но сам HTTP-код лучше держать в отдельном сервисе. Тогда его можно тестировать без браузера и переиспользовать в нескольких сценариях.
| Подход | Когда подходит | Цена решения |
|---|---|---|
| Запрос прямо в контроллере | Быстрый прототип с одним вызовом | Логика смешивается с HTTP, сложнее тестировать и повторять |
| Отдельный сервис | Постоянная интеграция, несколько мест вызова | Нужно создать класс и описать зависимость в services.yml |
| Очередь Drupal | Синхронизация, массовая отправка, неинтерактивные операции | Результат становится асинхронным, требуется мониторинг очереди |
| Внешний интеграционный слой | Много систем, сложная маршрутизация и трансформация данных | Появляется отдельная инфраструктура и ещё одна точка отказа |
Для обычной интеграции начните с сервиса. Если пользователь не должен ждать ответа партнёра, добавьте очередь. Не отправляйте длинную синхронизацию из submit-обработчика формы: браузерный запрос может закончиться раньше, чем внешний API.
Соберите сервис на Guzzle
В Drupal 10.x и 11.x HTTP-клиент доступен через сервис http_client. Ниже показана схема для собственного модуля partner_api. Версии Drupal и PHP в проекте сверяйте с официальной документацией: код использует стандартный сервис ядра, но состав зависимостей проекта может отличаться.
Опишите сервис в services.yml
Файл partner_api.services.yml должен находиться в корне модуля. В сервис передаём HTTP-клиент и конфигурацию модуля.
services:
partner_api.client:
class: Drupal\partner_api\Service\PartnerApiClient
arguments:
- '@http_client'
- '@config.factory'
После изменения файла очистите кэш контейнера Drupal. Проверка проста: модуль должен включаться без ошибки контейнера, а сервис можно получить через внедрение зависимости в другой класс.
Вынесите запрос в отдельный класс
Класс ниже отправляет JSON, задаёт таймауты, проверяет HTTP-ответ и не раскрывает тело запроса в обычном сообщении лога. Метод рассчитан на конкретную операцию отправки заказа; названия полей замените на контракт вашего API.
<?php
namespace Drupal\partner_api\Service;
use Drupal\Core\Config\ConfigFactoryInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Log\LoggerInterface;
final class PartnerApiClient {
public function __construct(
private ClientInterface $httpClient,
private ConfigFactoryInterface $configFactory,
private LoggerInterface $logger,
) {}
public function sendOrder(array $order): array {
$config = $this->configFactory->get('partner_api.settings');
$baseUrl = rtrim((string) $config->get('base_url'), '/');
$token = (string) $config->get('api_token');
if ($baseUrl === '' || $token === '') {
throw new \RuntimeException('Partner API is not configured.');
}
try {
$response = $this->httpClient->request('POST', $baseUrl . '/orders', [
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'json' => $order,
'connect_timeout' => 3.0,
'timeout' => 10.0,
'http_errors' => FALSE,
]);
}
catch (GuzzleException $exception) {
$this->logger->error('Partner API request failed: @message', [
'@message' => $exception->getMessage(),
]);
throw new \RuntimeException('Partner API is temporarily unavailable.', 0, $exception);
}
$status = $response->getStatusCode();
$body = (string) $response->getBody();
$data = json_decode($body, TRUE);
if ($status < 200 || $status >= 300 || !is_array($data)) {
$this->logger->warning('Partner API returned HTTP status @status.', [
'@status' => $status,
]);
throw new \RuntimeException('Partner API returned an invalid response.');
}
return $data;
}
}
В реальном проекте добавьте logger.factory в services.yml и внедрите канал логирования через фабрику. Если вы копируете класс буквально, объявите третий аргумент как зависимость, например '@logger.factory', а в конструкторе получите канал. Не записывайте токен и полный ответ API в лог: там могут оказаться персональные данные.
Разделяйте ошибки сети и ошибки API
Опция http_errors => FALSE позволяет обработать ответы 4xx и 5xx самостоятельно. Это важно: неверный токен и недоступный сервер требуют разных действий.
- Сетевой сбой или таймаут. Запрос не получил корректный ответ. Его можно повторить, если операция идемпотентна.
- 400 или 422. Данные не прошли проверку. Повторять тот же payload бессмысленно — сначала исправьте данные.
- 401 или 403. Проверьте токен, права и срок его действия. Автоматические повторы без изменения авторизации только создадут нагрузку.
- 404. Проверьте URL, версию API и идентификатор ресурса.
- 429. Площадка ограничила частоту запросов. Нужны задержка, очередь и учёт заголовка
Retry-After, если сервис его возвращает. - 500, 502 или 503. Это ошибка на стороне партнёра либо промежуточного прокси. Для безопасных операций используйте ограниченное число повторов.
Не делайте бесконечные ретраи. Три попытки подряд с короткой задержкой могут превратить временный сбой в лавину запросов. Для создания платежей, заявок и заказов перед повтором нужен уникальный ключ операции, если его поддерживает API.
Когда переводить интеграцию в очередь
Синхронный вызов уместен, если пользователь должен сразу увидеть результат: например, проверку адреса или расчёт тарифа. Очередь лучше для вебхуков, массовой выгрузки, отправки заказов и повторных попыток.
В Drupal для этого используют Queue API и обработчик, который берёт элемент очереди, вызывает ваш сервис и удаляет элемент только после успешного завершения. Точный код обработчика зависит от структуры модуля и версии проекта, поэтому не смешивайте в нём преобразование данных, HTTP-запрос и отображение сообщения пользователю.
Отдельно контролируйте зависшие элементы, число повторов и время последней ошибки. Запись «поставлено в очередь» не означает, что внешний API получил данные.
Что ломается чаще всего
Токен хранится в коде
Секрет попадает в Git, резервные копии и логи. Храните его в конфигурации с понятной политикой доступа или в секрет-хранилище окружения. Не экспортируйте секрет в обычный конфигурационный дамп Drupal.
Таймаут не задан
Без явного ограничения внешний сервис может удерживать PHP-запрос дольше, чем ожидает пользователь или прокси. Разделяйте connect_timeout и общий timeout, а значения подбирайте по SLA партнёра и лимиту времени вашего веб-сервера.
Ответ считается успешным по факту отсутствия исключения
При отключённой автоматической обработке HTTP-ошибок исключения может не быть даже при статусе 500. Проверяйте диапазон 2xx, тип JSON и обязательные поля ответа.
Один и тот же заказ отправляется повторно
Повтор возникает после таймаута: Drupal не знает, принял ли запрос партнёр. Сохраняйте внешний идентификатор операции, используйте идемпотентный ключ и разделяйте статусы «не отправляли», «отправляем», «подтверждено», «ошибка».
В лог попадают персональные данные
Полный payload удобен для отладки, но опасен в продакшене. Логируйте внутренний идентификатор сущности, HTTP-статус, длительность и код ошибки; тело ответа сохраняйте только по отдельному правилу и с маскированием.
Когда какой вариант выбирать
Если интеграция состоит из одного запроса и результат нужен в текущем HTTP-ответе, используйте отдельный сервис и синхронный вызов. Если вызов может длиться дольше нескольких секунд, допускает повторы или запускается для десятков сущностей, выносите операцию в очередь. Если требуется маршрутизировать данные между несколькими системами, преобразовывать схемы и хранить сложные состояния, оцените интеграционный слой вместо разрастания Drupal-модуля.
Что сделать дальше
После первого успешного запроса добавьте тестовый сценарий с недоступным хостом, неверным токеном, ответом 422 и статусом 429. Затем измеряйте длительность запроса, долю ошибок по кодам, размер очереди и количество повторов. Только после этого включайте интеграцию для всех пользователей и увеличивайте объём синхронизации.
Документацию конкретного API держите рядом с кодом модуля, а изменения контракта проверяйте на тестовом окружении. Инвалидация кеша, вебхуки и миграция уже отправленных данных — отдельные темы; здесь важнее сначала сделать один HTTP-вызов наблюдаемым, ограниченным по времени и безопасным для повтора.
Генератор изображений: Иллюстрация, обложка, баннер, схема — по текстовому описанию, без исходного фото.
Спросить за 20 ₽Источники: Drupal: Services and dependency injection, Guzzle: Request options
Частые вопросы
Можно ли вызвать сторонний API прямо из контроллера Drupal?
Какой таймаут поставить для API?
Нужно ли повторять запрос после таймаута?
Что делать, если API возвращает 429?
Что будет, если интеграция не успеет выполнить запрос в срок?
Подойдёт ли этот код для другой версии Drupal?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.