Интеграция Drupal с API: типичные ошибки и решения
Интеграция Drupal со сторонним API ломается не только из-за неверного токена. Таймауты, неожиданный JSON, повторные запросы и неправильное хранение секретов превращают обычный обмен данными в источник сбоев.

Интеграция Drupal со сторонним API часто работает в тестовой среде и ломается сразу после запуска: внешний сервис отвечает медленнее, возвращает другой статус или временно недоступен. В этой статье — практические приёмы для Drupal 10 и 11: как вызывать API через сервис Drupal, обрабатывать ошибки, не хранить ключи в конфигурации и не блокировать пользовательский запрос повторными попытками.
Сначала определите границу интеграции
До написания кода зафиксируйте четыре вещи: кто инициирует запрос, какие данные передаются, допустима ли задержка и что считать успешным результатом. У синхронного запроса из контроллера одна цена — пользователь ждёт ответ. У фоновой синхронизации через очередь другая: данные могут появиться через несколько минут, зато временная ошибка API не превращается в ошибку страницы.
Для Drupal обычно встречаются три сценария:
- получить данные во время формирования страницы;
- отправить данные после действия пользователя;
- регулярно забрать большой объём данных и сохранить его в Drupal.
Первый сценарий подходит для небольшого ответа, который можно быстро получить или взять из кеша. Массовый импорт лучше выполнять очередями или cron. Не переносите медленный внешний запрос в пользовательский путь только потому, что так проще начать.
Подходы и их цена
| Подход | Когда выбирать | Плюсы | Цена и риск |
|---|---|---|---|
| Запрос из контроллера | Нужен небольшой ответ сразу | Минимум кода, результат доступен в текущем запросе | Внешний таймаут блокирует страницу |
| Отдельный сервис Drupal | Интеграцию используют несколько модулей | Единые таймауты, логирование и тестирование | Нужно настроить dependency injection |
| Queue API и cron | Импорт, уведомления, повторная обработка | Пользователь не ждёт API, ошибки можно повторить | Появляется задержка и контроль состояния задач |
| Кеширование ответа | Данные меняются реже, чем открываются страницы | Меньше запросов и зависимость от доступности API | Данные могут быть не самыми свежими |
Практическое правило: синхронный вызов оставляйте для короткой операции с понятным лимитом времени. Всё, что можно выполнить позже, отправляйте в очередь. Инвалидация кеша — отдельная задача; здесь важен только выбор границы вызова.
Внешний API — не продолжение Drupal, а ненадёжная сеть за его пределами. Код должен переживать задержку, неполный ответ и временную недоступность сервиса.
Ошибка 1. Запрос размещён прямо в контроллере
Такой код быстро появляется в прототипе: контроллер создаёт HTTP-клиент, собирает заголовки и разбирает JSON. Затем тот же API понадобился cron-задаче и форме. В итоге таймауты и обработка ошибок расходятся по файлам.
В Drupal 10 и 11 используйте сервис и внедряйте в него стандартный HTTP-клиент. Ниже фрагмент класса: он не привязан к конкретному поставщику, поэтому URL и схему авторизации нужно заменить на требования вашего API.
<?php
namespace Drupal\my_module\Service;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use Psr\Http\Message\ResponseInterface;
final class ExternalApiClient {
public function __construct(
private ClientInterface $httpClient,
private LoggerChannelFactoryInterface $loggerFactory,
) {}
public function getItem(string $itemId, string $apiKey): ?array {
try {
$response = $this->httpClient->request('GET', 'https://api.example.com/v1/items/' . rawurlencode($itemId), [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $apiKey,
],
'connect_timeout' => 2.0,
'timeout' => 5.0,
'http_errors' => false,
]);
if ($response->getStatusCode() === 404) {
return null;
}
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
$this->logFailure($response);
return null;
}
$data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
return is_array($data) ? $data : null;
}
catch (\Throwable $exception) {
$this->loggerFactory->get('my_module')->error(
'External API request failed: @message',
['@message' => $exception->getMessage()]
);
return null;
}
}
private function logFailure(ResponseInterface $response): void {
$this->loggerFactory->get('my_module')->warning(
'External API returned HTTP @status',
['@status' => $response->getStatusCode()]
);
}
}
В сервисном описании укажите зависимость на http_client и логгер. Вызов request(), параметры таймаута и объект ответа относятся к Guzzle, который используется Drupal Core. Проверьте точный формат сервисного файла в документации вашей версии Drupal, если модуль создаётся с нуля.
Ошибка 2. Любой статус считают успехом
HTTP-ответ не равен успешной операции. Код 200 может содержать JSON с ошибкой бизнес-уровня, а код 204 — корректный ответ без тела. Отдельно обработайте хотя бы следующие группы:
- 2xx — операция принята или выполнена; проверьте, нужен ли вам JSON;
- 400–499 — проблема запроса, токена, прав или лимита клиента; без изменения запроса повторять обычно бессмысленно;
- 429 — превышен лимит; учитывайте заголовок
Retry-After, если поставщик его документирует; - 500–599 — сбой на стороне сервиса или промежуточной инфраструктуры; возможен ограниченный повтор;
- отсутствие ответа — таймаут, ошибка DNS или TLS; это не то же самое, что HTTP-статус.
Не записывайте в лог полный заголовок Authorization, тело запроса и персональные данные. Для расследования обычно достаточно идентификатора операции, URL без секретов, статуса, времени ожидания и корреляционного идентификатора, если его выдаёт поставщик.
Ошибка 3. Нет таймаутов и ограничений на повторы
Без connect_timeout процесс может долго ждать установления соединения. Без общего timeout зависший внешний сервис удерживает PHP-воркер. Эти параметры не должны быть «по умолчанию»: задайте их исходя из допустимого времени ответа страницы.
Повтор безопасен не всегда. Повторить чтение обычно проще, чем запрос на создание заказа. Если API поддерживает идемпотентный ключ1, передавайте его для операций, которые нельзя выполнить дважды. Если такой механизм не предусмотрен, очередь должна хранить состояние задачи, а не бездумно отправлять запрос заново.
Ограничьте число повторов и увеличивайте паузу между ними. Для синхронной страницы чаще разумнее показать запасной результат или понятное сообщение, чем трижды ждать один и тот же сервис. Для очереди повтор должен завершаться переводом задачи в состояние «ошибка» после заданного лимита.
Ошибка 4. Секреты попадают в конфигурацию и репозиторий
API-ключ нельзя помещать в исходный код, экспортируемую конфигурацию или пример запроса в документации. Конфигурация Drupal может попасть в Git, резервную копию или окружение, где ключ не должен находиться.
Храните секрет в переменных окружения, менеджере секретов или другом согласованном с инфраструктурой хранилище. В код передавайте уже полученное значение. В примерах используйте только заглушку YOUR_API_KEY. Если ключ уже оказался в репозитории, удаление строки не отменяет утечку: ключ нужно отозвать у поставщика и выпустить новый.
Сноска 1. Идемпотентность — свойство операции, при котором повтор того же запроса не меняет результат после первого успешного выполнения. Для создания сущностей это обычно требует специального ключа или уникального идентификатора операции.
Ошибка 5. JSON считают неизменным
Проверяйте не только факт декодирования, но и структуру данных. Поле может исчезнуть, стать null, изменить тип или прийти строкой вместо числа. Доверие к пути вроде $data['items'][0]['price'] без проверок заканчивается предупреждениями PHP или неверной записью в Drupal.
Разделяйте три состояния: корректный ответ, корректный ответ «ничего не найдено» и повреждённый или неожиданный ответ. Для обязательных полей добавьте валидацию и логируйте версию схемы или идентификатор ответа. Не выводите сырой ответ API пользователю: он может содержать внутренние сообщения поставщика и персональные данные.
Ошибка 6. Внешние данные записывают без защиты от дублей
Повторный запуск cron или повтор доставки webhook не должен создавать вторую сущность. Сохраните внешний идентификатор и сделайте поиск по нему перед записью. Если операция состоит из нескольких шагов, храните статус синхронизации: «получено», «обработано», «ошибка». Так можно продолжить работу после сбоя, а не запускать импорт с нуля.
Для больших объёмов не загружайте весь ответ в память и не обрабатывайте тысячи элементов в одном веб-запросе. Разбивайте импорт на страницы поставщика, кладите отдельные порции в очередь и измеряйте длительность каждой задачи.
Что ломается чаще всего
- 401 или 403. Проверьте срок действия ключа, схему заголовка и права конкретного аккаунта.
- 404. Убедитесь, что идентификатор закодирован для URL и используется правильная версия endpoint.
- 429. Снизьте частоту запросов, проверьте лимит поставщика и не запускайте несколько одинаковых cron-задач.
- JSON parse error. Сохраните статус и безопасную часть тела ответа: вместо JSON поставщик мог вернуть HTML страницы ошибки.
- Страница Drupal отвечает 504. Измерьте DNS, установление соединения и чтение ответа; затем вынесите операцию в очередь или сократите таймаут.
Как тестировать интеграцию до продакшена
Проверьте не только успешный ответ. Нужны сценарии с неверным ключом, пустым результатом, 404, 429, медленным соединением, некорректным JSON и временным 500. Для каждого сценария зафиксируйте: что увидит пользователь, будет ли повтор, попадёт ли задача в очередь и какая запись появится в логе.
На тестовом окружении используйте отдельный аккаунт поставщика и ограниченные права. Не проверяйте интеграцию реальными заказами или персональными данными. Перед выпуском измерьте количество запросов, долю ошибок, среднее и максимальное время ответа, размер очереди и возраст самого старого задания.
Что делать дальше
Вынесите HTTP-вызовы в отдельный сервис, задайте явные таймауты и классификацию статусов, затем добавьте защиту от дублей. После этого подключите очередь для медленных операций и алерты на 429, 5xx и рост времени ответа. В продакшене в первую очередь следите не за числом успешных запросов, а за долей задач, которые повторяются или остаются необработанными.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: Drupal.org: HTTP client, Guzzle Documentation: Request Options
Частые вопросы
Можно ли вызывать сторонний API прямо из Drupal-контроллера?
Какой таймаут выбрать для API?
Нужно ли повторять запрос после ошибки 500 или 429?
Что делать, если API вернул 200, но JSON имеет другую структуру?
Как хранить API-ключ в Drupal?
Что делать, если интеграция работает на тестовом стенде, но падает в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.