Перейти к содержимому
getnextpdf.com

Enterprise редакция

Webhook — углублённый справочник

Пространство имён NextPDF\Enterprise\Webhook предоставляет доставку webhook с изоляцией по арендаторам для событий задач. Публичная поверхность — это шесть символов: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy и DeadLetterEntry. Менеджер регистрирует конечные точки для каждого арендатора и диспетчеризует события задач в подписанные регистрации. Движок доставки отправляет POST-запросом полезную нагрузку JSON, подписанную HMAC-SHA256, проверяет каждый адрес назначения через шлюз исходящего трафика Core против SSRF, повторяет с экспоненциальной отсрочкой и записывает постоянные сбои в очередь недоставленных сообщений в памяти. Начиная с 3.1.0 подпись привязывает заголовок X-NextPDF-Timestamp к базовой строке MAC, поэтому получатели проверяют свежесть и целостность вместе. Руководство на уровне рабочего процесса см. в Webhook.

Эта возможность поставляется в NextPDF Enterprise (nextpdf/enterprise) и активируется лицензионным конвертом уровня Enterprise. Развёртывание без этого права не загружает классы возможности. Сравните редакции и получите лицензию.

Поверхность webhook — это базовая возможность Enterprise, доступная после установки пакета Enterprise; отдельного флага для каждой функции нет. У NextPDF Core (Apache-2.0) и NextPDF Pro нет поверхности регистрации или доставки webhook; менеджер, регистрация, полезная нагрузка, движок доставки, политика повторов и запись недоставленных сообщений поставляются только в nextpdf/enterprise.

СимволПараметрыПоведение по умолчаниюВозвращаетБросает или завершается сбоемПримечания
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullСоздаёт менеджер с пустым индексом регистраций в памятиНовый WebhookManagerНе бросаетРегистрации индексируются по арендатору
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationДобавляет регистрацию в индекс вызывающего арендатораvoidInvalidArgumentException, когда арендатор регистрации не совпадает с арендатором контекстаМежарендаторная регистрация отклоняется до сохранения
WebhookManager::unregisterTenantContext $tenant, string $registrationIdЗаменяет совпадающую регистрацию деактивированной копиейboolНе бросает; возвращает false, когда id не найденМягкая деактивация; история сохраняется
WebhookManager::activeRegistrationsTenantContext $tenantОтфильтровывает регистрации арендатора до активныхlist<WebhookRegistration>Не бросаетВидны только регистрации вызывающего арендатора
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventДоставляет событие каждой активной регистрации, подписанной на этот тип событияint (успешные доставки)Пробрасывает JsonException, когда данные события не кодируются в JSON; сбои доставки не бросаютДля каждой доставки регистрации генерируется новый 32-символьный шестнадцатеричный id доставки
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullСохраняет переданные значения дословноНовый WebhookRegistrationОбъявленного @throws нет; PHP выбрасывает TypeError при несовпадении типов аргументов под strict_typesfinal readonly; пустой $events означает подписку на всё
WebhookRegistration::subscribesToJobEventType $eventTypetrue, когда $events пуст или содержит этот типboolНе бросаетСтрогое сравнение по идентичности
WebhookRegistration::deactivateВозвращает неактивную копиюselfНе бросаетИсходный экземпляр не изменяется
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdКопирует id задачи, тип события, данные и временную метку из событияselfНе бросаетСтатическая фабрика, используемая dispatch
WebhookPayload::toJsonСериализует тело из шести полей с неэкранированными слешамиnon-empty-stringJsonException, когда данные события не кодируются в JSONJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayВозвращает тело как ассоциативный массивarray<string, mixed>Не бросаетВременная метка в расширенном формате RFC 3339
WebhookPayload::signedTimestampВремя события в секундах Unix, ограниченное снизу нулёмint<0, max>Не бросаетВыдаётся как X-NextPDF-Timestamp и привязывается к MAC
WebhookPayload::signstring $secretHMAC-SHA256 по базовой строке {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException через toJson(), когда тело не кодируетсяКриптографически привязывает заголовок временной метки к телу
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullДвижок доставки PSR-18/PSR-17 с пустой очередью недоставленных сообщенийНовый WebhookDeliveryНе бросаетПолитика по умолчанию: 5 попыток, база 1 с, потолок 300 с
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadОтправляет POST-запросом подписанную полезную нагрузку с проверкой исходящего трафика против SSRF на каждой попытке и экспоненциальной отсрочкойboolJsonException до первой попытки, когда тело не кодируется; иначе не бросает — false означает, что полезная нагрузка направлена в очередь недоставленных сообщенийtrue только при ответе 2xx
WebhookDelivery::deadLettersВозвращает все записанные записиlist<DeadLetterEntry>Не бросаетВ памяти, в рамках процесса
WebhookDelivery::clearDeadLettersОчищает очередь недоставленных сообщенийvoidНе бросаетНеобратимо; сначала экспортируйте записи, если требуется повтор
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Сохраняет значения политикиНовый WebhookRetryPolicyОбъявленного @throws нет; параметры документированы как positive-int$maxRetries считает общее число попыток
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), с ограничением maxDelaySecondspositive-intНе бросаетНомера попыток начинаются с 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue, пока текущая попытка ниже максимумаboolНе бросаетОжидание пропускается после последней попытки
WebhookRetryPolicy::default5 попыток, база 1 с, потолок 300 сselfНе бросаетСтатическая фабрика; значение по умолчанию для продакшена
WebhookRetryPolicy::aggressive10 попыток, база 2 с, потолок 600 сselfНе бросаетСтатическая фабрика для критичных конечных точек
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseСохраняет запись о сбое дословноНовый DeadLetterEntryОбъявленного @throws нет; TypeError под strict_typesfinal readonly; null в $lastHttpStatus означает сбой транспорта
DeadLetterEntry::markReplayedВозвращает копию с replayed = trueselfНе бросаетТот же id; исходная запись не изменяется
public function __construct(
private readonly WebhookDelivery $delivery,
private readonly ?LoggerInterface $logger = null,
) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): int
public function __construct(
public string $id,
public string $tenantId,
public string $url,
public array $events,
public string $secret,
public bool $active = true,
public ?string $description = null,
) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): self
public static function fromJobEvent(
JobEvent $event,
string $tenantId,
string $deliveryId,
): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): string
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(),
private readonly ?LoggerInterface $logger = null,
) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): void
public function __construct(
public int $maxRetries = 5,
public int $baseDelaySeconds = 1,
public int $maxDelaySeconds = 300,
) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): self
public function __construct(
public string $id,
public string $registrationId,
public WebhookPayload $payload,
public int $attempts,
public string $lastError,
public ?int $lastHttpStatus,
public DateTimeImmutable $failedAt,
public bool $replayed = false,
) {}
public function markReplayed(): self
  • Регистрации индексируются по арендатору. register() отклоняет регистрацию, чей идентификатор арендатора не совпадает с вызывающим контекстом. unregister() — это мягкая деактивация: регистрация заменяется неактивной копией, что сохраняет историю, но исключает её из будущей диспетчеризации.
  • dispatch() перебирает только активные регистрации вызывающего арендатора, подписанные на отправляемый тип события. Пустой список подписанных событий означает подписку на всё. Возвращаемое значение считает успешные доставки.
  • Каждая доставка — это HTTP POST с телом JSON и пятью заголовками: Content-Type: application/json, X-NextPDF-Signature (sha256=<hex>), X-NextPDF-Timestamp (секунды Unix), X-NextPDF-Delivery-Id и X-NextPDF-Event.
  • Поля тела JSON: delivery_id, job_id, event_type, data, timestamp (расширенный RFC 3339) и tenant_id, сериализованные с неэкранированными слешами. Значения типа события берутся из JobEventType в nextpdf/core: progress, completed, failed, cancelled.
  • Схема подписи (изменена в 3.1.0, ломающее изменение). Базовая строка HMAC-SHA256 — это {signedTimestamp}.{jsonBody} с ключом — секретом регистрации, а не одно тело. Значение X-NextPDF-Timestamp — это компонент временной метки MAC, поэтому подделанный или воспроизведённый заголовок временной метки делает подпись недействительной.
  • Проверка на стороне получателя: прочитайте заголовок X-NextPDF-Timestamp T; отклоните, когда T выходит за пределы допустимого окна свежести (например, 300 с); пересчитайте hash_hmac('sha256', T . '.' . rawBody, secret) по сырым полученным байтам; сравните за постоянное время со значением заголовка после удаления префикса sha256=.
  • Тело, подпись и идентификатор доставки вычисляются один раз на доставку и остаются постоянными на всех попытках повтора.
  • Шлюз исходящего трафика против SSRF. Перед каждой попыткой URL назначения проходит через шлюз Core UrlValidator::validateExternalUrl(): только схема HTTPS; диапазоны loopback, приватные, зарезервированные, carrier-grade-NAT, облачных метаданных и переходные диапазоны IPv6 со встроенным IPv4 блокируются; имена хостов разрешаются через DNS (A и AAAA), а неразрешимые хосты отклоняются fail-closed. Заблокированный URL никогда не отправляется: цикл попыток прерывается, и полезная нагрузка направляется прямо в очередь недоставленных сообщений с последней ошибкой Blocked SSRF destination: и нулевым статусом HTTP.
  • Классификация результата каждой попытки: 2xx — успех и немедленный возврат; 4xx, кроме 429, — окончательный и сразу попадает в недоставленные сообщения; любой другой результат — 3xx, 429, 5xx или исключение транспорта — повторяемый вплоть до общего числа попыток политики.
  • Отсрочка экспоненциальная: ожидание перед следующей попыткой равно baseDelaySeconds × 2^(attempt − 1), с ограничением maxDelaySeconds. Ожидание пропускается после последней попытки.
  • Когда ни одна попытка не удаётся, DeadLetterEntry записывает уникальный id, id регистрации, исходную полезную нагрузку, число попыток (ограниченное максимумом политики), последнее сообщение об ошибке, последний статус HTTP (null при сбое транспорта или блокировке SSRF) и временную метку сбоя.
  • Очередь недоставленных сообщений хранится в памяти и ограничена временем жизни процесса. markReplayed() создаёт помеченную копию; он не пересылает повторно, и очередь сохраняет исходную запись.
  • Пустой список событий. Регистрация получает каждый тип события. Ограничьте список явно, если получатель не должен видеть все события.
  • Окончательный 4xx против сбоя транспорта. Отклонение 4xx записывает заполненный lastHttpStatus; сбой соединения записывает null. Используйте null, чтобы отличить отклонение получателем от сбоя транспорта.
  • Адрес назначения, заблокированный SSRF. Регистрация, указывающая на адрес HTTP, приватный, loopback или метаданных, попадает в недоставленные сообщения на первой попытке с ошибкой Blocked SSRF destination: и нулевым статусом. Исходящий запрос не выполняется. Исправьте URL и зарегистрируйте заново.
  • Устаревшие получатели после обновления. Получатель, всё ещё проверяющий HMAC только по телу до версии 3.1.0, завершается сбоем fail-closed против доставок 3.1.0. Переведите получателя на базовую строку {timestamp}.{body} и обрабатывайте X-NextPDF-Timestamp.
  • Некодируемые данные события. toJson() и sign() бросают JsonException, который пробрасывается из deliver() и dispatch() до выполнения любой попытки.
  • Синхронная блокировка. deliver() засыпает встроенно между попытками. Суммарная отсрочка достигает 15 с при политике по умолчанию и около 17 минут при агрессивной политике. Диспетчеризуйте из обработчика очереди, когда задержка получателя не заслуживает доверия.
  • Ограничение числа попыток. Записанное число попыток никогда не превышает максимум политики, хотя внутренний счётчик цикла продвигается за него при исчерпании.
  • Рост очереди и устойчивость. Очередь недоставленных сообщений растёт неограниченно в рамках процесса и исчезает при перезапуске. Экспортируйте записи через deadLetters() и сохраняйте их внешне перед вызовом clearDeadLetters(), когда требуется устойчивый повтор.
  • Повтор управляется оператором. Повторная доставка означает повторный вызов deliver() с полезной нагрузкой записи; markReplayed() лишь фиксирует факт на копии.
  • Остаточный риск DNS-rebinding. URL повторно проверяется на каждой попытке, что сужает, но не закрывает окно rebinding: абстракция PSR-18 не может закрепить соединение за проверенным IP. Добавьте средства контроля исходящего трафика на сетевом уровне там, где этот остаточный риск важен.
  • Обращение с секретом. Секрет регистрации — это учётные данные. HMAC аутентифицирует только целостность и происхождение — это не конфиденциальность. Не помещайте в полезную нагрузку события данные, которые получатель не должен видеть.

Подписание полезной нагрузки — это HMAC-SHA256 через hash_hmac() в PHP, поэтому оно опирается на криптопровайдер хоста. В сборке с ограничениями FIPS неодобренный примитив завершается сбоем на криптографической границе, а не понижает уровень. Слой webhook не добавляет собственной криптографической политики.

  • Аутентификация полезной нагрузки реализует HMAC — код аутентификации сообщений с ключом-хешем из FIPS PUB 198-1 §1, инстанцированный с SHA-256.
  • Защита от повторов следует рекомендациям OWASP Cheat Sheet Series по безопасности webhook: временная метка события передаётся в отдельном заголовке и включается в вычисление подписи, поэтому подделанная временная метка не проходит проверку.
  • Временные метки тела используют расширенный формат даты-времени RFC 3339. Объявлено в коде: RFC 3339 не извлекался из корпуса RAG для этой страницы.
  • Это утверждения о возможностях, основанные на исходном коде продукта и процитированных пунктах. NextPDF не делает заявлений о соответствии или сертификации для этой поверхности.
  • Все классы объявляют strict_types=1 и являются final; WebhookRegistration, WebhookPayload, WebhookRetryPolicy и DeadLetterEntry — это final readonly с продвинутыми публичными свойствами.
  • Модуль несёт аннотацию @since со значением 2.2.0; схема подписи с привязкой к временной метке — это документированное ломающее изменение в 3.1.0.
  • Движок доставки принимает абстракции PSR-18/PSR-17, поэтому мок HTTP-клиента проходит весь путь отправки, повтора и недоставленных сообщений офлайн. Логгер по умолчанию null; внедрите логгер PSR-3 в продакшене, иначе сбои проявляются только через возвращаемые значения.
  • Реализации получателей должны использовать hash_equals() для сравнения подписи и применять окно свежести к X-NextPDF-Timestamp.
  • Рекомендуемые граничные тесты: регистрация с несовпадающим арендатором, веерная рассылка при пустом списке событий, окончательный 4xx, исчерпание повторов, URL, заблокированный SSRF, отклонение подписи с подделанной временной меткой против фиксированного вектора и ограничение числа попыток в недоставленных сообщениях.

Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы тикетов выходят за рамки.