События

Доставка на localhost

Как агент apistend listen приносит события на вашу машину и чем это отличается от поведения боевых сервисов.

На этой странице · 6

Боевые сервисы шлют вебхуки на публичный адрес: обработчик должен быть виден из интернета. На машине разработчика его нет, и обычный путь — поднять туннель, получить временный домен, вписать его в настройки портала, а после перезапуска туннеля вписать заново.

APIStend решает это иначе: события забирает агент, который вы запускаете сами.

npx apistend listen --forward localhost:3000/webhooks

Как это устроено

Соединение всегда инициирует агент. Он поднимает WebSocket к серверу APIStend, и события приходят кадрами в уже открытое соединение. Входящих подключений к вашей машине нет — поэтому схема работает из-за NAT и корпоративного файрвола, и ей не нужны ни публичный адрес, ни проброс портов.

  1. 1

    Агент создаёт сессию

    POST /v1/tunnel/sessions серверным ключом stend_sk_…. В ответ приходит одноразовый токен подключения со сроком жизни 10 минут и адрес ws(s)://…/v1/tunnel/connect.

  2. 2

    Агент подключается по WebSocket

    Токен передаётся заголовком Authorization: Bearer, подпротокол — apistend.tunnel.v1. Токен гасится при первом использовании: двумя параллельными подключениями одним токеном воспользоваться нельзя.

  3. 3

    Сервер отвечает кадром ready

    В нём идентификатор сессии для интерфейса (tnl-ef80), секрет подписи и число событий, накопившихся за простой.

  4. 4

    События идут кадрами delivery

    Агент отвечает ack сразу по получении и response — когда приложение ответило (или не ответило).

Почему это безопасно

В протоколе туннеля нет абсолютных адресов. Сервер присылает только относительный путь (/bitrix/deal), базу подставляет агент из своего --forward. Поэтому сервер физически не может заставить агента пойти в чужую сеть, а сам агент не превращается в открытый прокси.

Адрес пересылки проверяется дважды — в CLI и на сервере — и допускает только loopback: localhost, 127.0.0.1, ::1 и имена, оканчивающиеся на .localhost.

✗ Адрес пересылки должен быть локальным: получено example.com/hook
  Разрешены localhost, 127.0.0.1 и ::1

По той же причине подписка на локальную доставку хранит путь, а не URL: в форме создания вебхука вы указываете /bitrix/deal, а не полный адрес. Иначе склейка дала бы http://localhost:3000/webhooks/webhooks/bitrix/deal.

Очередь на время простоя

Если агента нет, доставка остаётся в очереди в состоянии queued. При подключении сервер досылает накопленное — до 200 доставок за один проход, в порядке поступления. Сколько их, агент печатает в баннере:

> В очереди 2 события за время простоя, отправляю

Считаются только доставки, которые ждут именно агента: события на публичный адрес отправляет сам сервер, обещать их в этой строке было бы неправдой.

Флаг `--no-replay` очередь не отменяет

Флаг объявлен у команды listen, но влияет он только на строку баннера: сервер досылает накопленное при подключении в любом случае. Проверено вживую — с --no-replay события из очереди приходят так же.

Одна сессия на песочницу

Активная сессия у песочницы ровно одна. Второй запущенный apistend listen вытесняет первый — тот получает код закрытия 4003 и завершается:

✗ Сессия занята другим агентом
  Закройте другой apistend listen или используйте другую песочницу

На этот код агент сознательно не переподключается: иначе два открытых терминала бесконечно выбивали бы друг друга.

Сервер пингует агента раз в 20 секунд. Если ответа нет дольше двух интервалов, соединение обрывается принудительно, чтобы в интерфейсе не висело «Подключено» у мёртвого сокета.

Чем это отличается от боя

Локальная доставка — расширение APIStend, а не воспроизведение поведения боевых сервисов. Отличия стоит держать в голове:

Боевой сервисAPIStend
Адрес обработчикапубличный, доступен из интернетапуть на вашей машине
Кто инициирует соединениесервисваш агент
Что настраивается на стороне сервисаURL обработчикапуть и --forward агента

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

Что проверить перед выходом в бой

Обработчик, отлаженный на локальной доставке, в бою окажется на публичном адресе. Проверьте то, чего на localhost не было: доступность извне, TLS и проверку подписи на реальном теле запроса.

Подводные камни

  • Фильтр --events молчит. Отсеянные события агент не пересылает и не отвечает на них серверу: доставка остаётся без ответа и через таймаут сервиса попадает в журнал как no_response. Строка баннера при этом всё равно пишет «События: все».
  • Локальный получатель без агента копит очередь. Одиночный trigger поставится в очередь молча; серия событий в этом случае просто откажется стартовать.
  • Путь склеивается с --forward. --forward localhost:3000/webhooks и путь подписки /bitrix/deal дают http://localhost:3000/webhooks/bitrix/deal.