Провайдер сообщает: delivery successful. В логах вашего обработчика пусто, запись в базе не появилась, а нужная бизнес-операция не произошла. Команда проверяет URL, перезапускает приложение и отправляет событие ещё раз — иногда создавая дубликат вместо решения проблемы.
Такой инцидент удобно называть webhook black hole: запрос как будто вошёл в систему и исчез. Но вебхуки не пропадают в одной абстрактной «сети». Между отправителем и бизнес-операцией есть несколько отдельных границ, и на каждой слово success означает что-то своё.
Провайдер
→ DNS
→ TLS
→ firewall / CDN / load balancer
→ reverse proxy
→ приложение
→ очередь или база данных
→ бизнес-операция
Чтобы найти пропавший вебхук, сначала нужно определить последнюю границу, прохождение которой подтверждено фактами. Разберём, что именно может скрываться за зелёным статусом и как проверить пять частых причин: DNS, TLS, firewall, тайм-аут около HTTP-ответа и падение приложения после ACK.
Что на самом деле означает delivery successful
У статуса успешной доставки нет универсального определения. Разные сервисы могут называть успехом разные события:
событие создано во внутренней системе провайдера;
сообщение принято его очередью на отправку;
одна из попыток получила HTTP-ответ класса
2xx;последняя попытка завершилась успешно, хотя предыдущие были неудачными;
получатель подтвердил запрос, но ещё не выполнил бизнес-операцию.
Например, 202 Accepted обычно сообщает только о принятии задачи в асинхронную обработку. Даже 200 OK или 204 No Content подтверждают лишь HTTP-ответ. Они не доказывают, что приложение записало платёж, обновило заказ, сохранило событие в очереди или завершило транзакцию.
Поэтому первый вопрос к провайдеру или собственной системе отправки должен звучать не «почему вы показываете success?», а точнее:
Какое конкретное событие переводит доставку в успешный статус и к какой попытке относится этот статус?
Если ответ — «настроенный URL вернул 2xx», круг поиска резко сужается. Полный сбой DNS-разрешения, TLS handshake или прямое блокирование соединения firewall не могли одновременно помешать этой же попытке и позволить отправителю получить HTTP-ответ от конечного приложения. Значит, ответил другой адрес, промежуточный компонент либо само приложение — раньше, чем надёжно приняло событие.
DNS issue: запрос ушёл не туда
DNS-сбой не всегда выглядит как NXDOMAIN или ошибка разрешения имени. В таком случае доставка действительно завершится неуспешно: отправитель не сможет установить HTTP-соединение.
Более коварный сценарий — домен успешно разрешился, но не в тот адрес:
после миграции сохранилась старая DNS-запись;
разные резолверы ещё видят разные значения из-за TTL;
запись указывает на старый load balancer или прежнее окружение;
IPv4 и IPv6 ведут к разным конфигурациям;
webhook URL содержит правильный домен, но неправильный регион, поддомен или путь;
redirect отправляет запрос на другой hostname.
Старый сервер вполне может вернуть 200 OK, поэтому провайдер покажет успешную доставку. В логах актуального backend ничего не будет — запрос до него не дошёл.
Проверять нужно не только доменное имя в настройках, но и фактический маршрут конкретной попытки:
Скопируйте webhook URL из настроек провайдера, не из документации или локального
.env.Уточните, следует ли отправитель redirects и какой URL стал итоговым.
Сравните DNS-ответы из нескольких сетей или регионов с адресами актуального балансировщика.
Проверьте записи
AиAAAA, TTL и недавние изменения DNS.Найдите запрос в access-логах всех хостов, которые ещё могут обслуживать старый адрес.
Здесь особенно полезен уникальный идентификатор события. Поиск только по времени ненадёжен: часы могут различаться, а повторная попытка — произойти через несколько минут.
TLS issue: защищённое соединение завершилось не там, где вы думаете
Если TLS handshake с настроенным URL не состоялся, HTTP-запрос ещё не начался. Типичные причины — истёкший сертификат, несовпадение hostname, неполная цепочка сертификатов, неподдерживаемая версия TLS, ошибка SNI или проблемы с mutual TLS. Корректный журнал доставки должен показывать такую попытку как неуспешную, а не как delivery successful.
Но во многих production-схемах TLS завершается не в приложении:
Провайдер → HTTPS → CDN / load balancer → HTTP или HTTPS → backend
Внешний TLS может работать нормально, пока соединение от load balancer к backend не устанавливается. Если пограничный компонент при этом возвращает собственный 2xx — например, подтверждает помещение запроса во внутреннюю очередь, — отправитель видит успех, хотя приложение не получило запрос.
При диагностике разделяйте два соединения:
TLS между отправителем и публичным endpoint;
TLS или HTTP между edge-компонентом и приложением.
Проверьте сертификат, hostname, SNI и цепочку на публичной стороне, затем отдельно изучите upstream-ошибки балансировщика. Успешный внешний handshake ничего не говорит о доступности внутреннего сервиса.
Firewall, WAF и load balancer: edge принял, backend не увидел
Прямое блокирование IP отправителя firewall обычно приводит к connection timeout или connection refused. Получить 2xx от заблокированного приложения в той же попытке невозможно.
Однако firewall редко является единственным компонентом на пути. Перед приложением могут находиться CDN, WAF, API gateway, ingress или reverse proxy. Тогда появляются две разные границы:
Отправитель → публичный edge → внутренний backend
Публичный edge может быть доступен, а внутренний маршрут — заблокирован security group, сетевой политикой или правилом firewall. Возможны и другие варианты:
WAF применил правило к webhook payload;
load balancer направил запрос в неверный target group;
ingress не знает указанный host или path;
health check считает инстанс доступным, хотя webhook handler на нём не работает;
edge отвечает до завершения передачи во внутреннюю систему;
allowlist содержит неактуальные исходящие IP провайдера.
Обычная блокировка на уровне WAF чаще приводит к ответу 4xx, а не к зелёному статусу. Если при этом dashboard остаётся зелёным, значит success относится к другой попытке, обозначает иной этап доставки либо ошибка была скрыта промежуточным компонентом.
Сопоставляйте стабильный event ID или delivery ID, а также request ID или trace ID, если он передаётся через все слои. Ищите идентификаторы последовательно в логах CDN/WAF, load balancer, reverse proxy и приложения. Если событие видно на edge, но отсутствует на следующем участке, black hole находится между этими двумя компонентами. Общий статус провайдера здесь почти ничего не добавляет.
Особенно осторожно относитесь к правилам, которые превращают внутреннюю ошибку в успешный HTTP-ответ. Такой fallback делает dashboard зелёным и одновременно скрывает отказ upstream.
Timeout около ответа: отправитель и backend видят разные результаты
Тайм-аут создаёт один из самых неоднозначных сценариев доставки. Приложение могло получить запрос и выполнить операцию, но отправитель не успел получить полный HTTP-ответ до своего deadline.
1. Backend получил webhook
2. Изменил данные
3. Начал возвращать 204 No Content
4. Ответ потерялся или пришёл слишком поздно
5. Отправитель зафиксировал timeout и сделал retry
6. Retry получил 2xx, и итоговый статус стал successful
Если dashboard показывает только финальный результат, команда видит successful, хотя первая попытка завершилась тайм-аутом. Если искать логи только по времени успешной попытки, можно не найти операцию: она была выполнена раньше. И наоборот, ручной повтор «пропавшего» вебхука может второй раз выполнить уже завершённое действие.
Поэтому при timeout нельзя делать вывод «backend ничего не получил». Тайм-аут означает лишь, что отправитель не получил ожидаемый ответ вовремя. Проверяйте все попытки, их номера, время начала, длительность и стабильный event ID. До ручного retry убедитесь, что операция не была зафиксирована в базе, очереди или внешней системе.
Защита от этого класса сбоев — идемпотентная обработка. Повтор одного события не должен создавать второй платёж, заказ или уведомление. Подробнее этот принцип разобран в статье «Почему вебхуки нужно обрабатывать идемпотентно».
App crash after ACK: приложение подтвердило то, чего ещё не сохранило
Это самый прямой ответ на загадку «successful, но операции нет». Обработчик вернул 2xx, а затем попытался выполнить работу:
получить webhook → вернуть 200 OK → положить в память → обработать позже
Если процесс упал после ACK, но до надёжной записи, отправитель не станет повторять доставку: с его стороны она уже успешна. Событие действительно исчезнет внутри принимающего приложения.
Похожий эффект возникает, когда handler:
запускает фоновую goroutine, promise или task без durable queue;
пишет событие в буфер, который сбрасывается асинхронно;
подтверждает запрос до commit транзакции;
игнорирует ошибку публикации во внутреннюю очередь;
возвращает
2xxизfinally, recovery middleware или общего error handler;отвечает успешно после валидации, но до принятия ответственности за дальнейшую обработку.
Безопасная граница ACK проходит не обязательно после всей бизнес-операции, но после надёжного принятия события. Например, приложение может сначала атомарно сохранить webhook или поместить его в durable queue, а затем быстро вернуть 2xx. Дальнейшая обработка остаётся асинхронной, однако перезапуск процесса уже не уничтожит единственную копию события.
Практическое правило простое:
Возвращайте
2xxтолько после того, как система действительно приняла ответственность за событие.
Что именно считается принятием ответственности, зависит от архитектуры: commit в базе, подтверждённая публикация в durable queue или другая операция, переживающая падение процесса.
Как найти пропавший вебхук: пошаговый чек-лист
Не начинайте с повторной отправки. Сначала соберите идентификаторы и восстановите одну конкретную попытку.
1. Зафиксируйте событие
Нужны event ID провайдера, delivery ID, точное время с часовым поясом, тип события и URL из конфигурации. Если стабильного идентификатора нет, сохраните хеш payload и несколько полей, по которым можно отличить событие от похожих.
2. Раскройте значение success
Уточните HTTP-статус, номер попытки, длительность, итоговый URL после redirects и наличие предыдущих ошибок. Статус «сообщение принято в очередь» нельзя использовать как доказательство доставки.
3. Определите последний подтверждённый слой
Есть DNS-ошибка — HTTP-запрос не дошёл до публичного endpoint.
Есть TLS-ошибка — соединение остановилось до HTTP.
Edge записал запрос, а reverse proxy нет — проверяйте маршрут между ними.
Proxy записал
2xx, а приложение не видит запрос — выясните, кто сформировал ответ и в какой upstream ушёл запрос.Приложение записало вход, но нет бизнес-результата — проверяйте ACK, транзакцию, очередь и падение процесса.
4. Сопоставьте все попытки
Не ограничивайтесь последней зелёной строкой. Первая попытка могла изменить состояние и завершиться тайм-аутом, а retry — только подтвердить уже обработанный event ID.
5. Только потом запускайте retry
Перед повтором проверьте идемпотентность обработчика и состояние бизнес-операции. Повторная доставка — диагностический и восстановительный инструмент, а не безопасная по умолчанию кнопка.
Как Adal делает границы доставки видимыми
Во входящем потоке Adal разделяет приём вебхука и его доставку в приложение:
Внешний сервис → Сервер Adal → сохранённый Request → Delivery → Destination
Сначала внешний сервис отправляет запрос на публичный HTTPS URL Сервера Adal. Принятый запрос сохраняется как Request, поэтому в панели можно проверить метод, путь, query-параметры, заголовки, тело и время получения независимо от состояния конечного обработчика.
Для каждой Destination Adal ведёт отдельную историю попыток доставки. В ней видны номер и время попытки, статус, HTTP-код ответа, длительность, а при неуспехе — сведения о сетевой, DNS, TLS или другой ошибке. Это позволяет отличить «Adal не смог разрешить домен» от «Destination вернула 500» и от «Destination ответила 2xx, но её внутренняя операция не произошла».
Граница здесь явная: успешная Delivery означает, что Adal получил от Destination HTTP-ответ класса 2xx. Она не доказывает, что приложение завершило бизнес-операцию, сохранило данные или корректно обработало payload. После успешного ответа автоматический retry не планируется, поэтому приложение должно подтверждать запрос только после надёжного принятия ответственности за него.
Для приложений, которые сами отправляют вебхуки клиентам, Adal Outbound сохраняет историю исходящих попыток. Диагностические данные помогают разделить DNS resolution, установление соединения, TLS, ожидание первого байта, общее время запроса, redirects и итоговый HTTP-статус. При этом 202 Accepted от Outbound означает только, что Adal принял сообщение в очередь; фактический результат появляется позже в истории доставки.
Adal не заменяет логи, очередь и мониторинг принимающего приложения. Он делает наблюдаемой транспортную часть пути и оставляет чёткую границу: где запрос был принят, куда его доставляли и какой ответ вернула Destination. Благодаря этому webhook black hole превращается из спора «мы отправили — мы не получили» в последовательность проверяемых участков.
Главное: зелёный статус должен иметь точную границу
Webhook delivery successful — ещё не конец истории. Это полезный сигнал только тогда, когда известно, кто именно ответил, на какой URL, в какой попытке и что система считает успехом.
DNS и TLS могут остановить запрос до HTTP. Firewall или неверный маршрут могут оставить его между edge и backend. Тайм-аут может скрыть уже выполненную операцию и вызвать повтор. А ранний ACK способен подтвердить событие за секунду до того, как падение приложения уничтожит его единственную недолговечную копию.
Не ищите один универсальный black hole. Найдите последнюю подтверждённую границу, свяжите попытки одним event ID и двигайтесь по маршруту дальше. Тогда «успешно доставлено, но ничего не получено» становится не парадоксом, а конкретным техническим инцидентом с проверяемой причиной.