Webhooks & Replay
Описание
Общее описание функционала
Повтор запроса (Replay) позволяет безопасно отправить заново входящий HTTP-запрос, который был зафиксирован инспектором трафика на вашем туннеле — например, вызов вебхука от внешнего сервиса. Запрос уходит только на настроенную цель этого туннеля; произвольные внешние адреса недоступны.
Как пользователь может использовать
- Разработчик получил вебхук с ошибкой 500, находит событие в инспекторе и повторяет запрос на локальный сервис после исправления кода.
- Пользователь меняет путь или добавляет тестовое тело запроса, чтобы воспроизвести сценарий без повторного вызова извне.
- Пользователь сравнивает код ответа и время ответа после повтора с исходным событием.
- Владелец туннеля проверяет обработчик вебхука на staging-окружении через туннель.
Как это реализовано в сервисе
Сервис восстанавливает метод, путь и безопасные заголовки из сохранённого события, применяет ваши правки и отправляет запрос через тот же туннель на его целевой адрес. Если на тарифе включён захват тел запросов и тело запроса сохранено целиком, оно подставляется автоматически; иначе тело можно указать вручную в форме повтора или в overrides.body. Обрезанное сохранённое тело не отправляется без явной подстановки. Учётные данные, cookies и служебные заголовки удаляются. Результат повтора (код ответа и длительность) возвращается сразу в интерфейсе.
Справочник API
API
Повтор запроса
POST /api/v1/inspector/events/{eventId}/replay
Требуется авторизация и доступ к туннелю события. Доступна на тарифах с функцией «Webhooks & Replay».
Тело запроса
| Поле | Описание |
| --- | --- |
| mode | Необязательно, устарело. Значения cloud и local эквивалентны — запрос идёт на цель туннеля. |
| overrides.method | Необязательный HTTP-метод. |
| overrides.path | Необязательный путь и query (должен начинаться с /). Заменяет путь и query из события. |
| overrides.headers | Необязательные дополнительные заголовки (без cookies, Authorization, Host и др.). |
| overrides.body | Необязательное тело запроса (до 32 КБ). Если не указано и для события сохранено полное тело запроса (захват тел на тарифе), используется сохранённое значение. При обрезанном сохранённом теле поле обязательно. |
Успешный ответ 200
| Поле | Описание |
| --- | --- |
| event_id | Идентификатор исходного события. |
| downstream_status | HTTP-код ответа целевого сервиса. |
| duration_ms | Время доставки в миллисекундах. |
| delivered | true, если ответ от цели получен. |
Подробнее — в разделе ограничения повтора вебхука.
Ошибки
Ошибки
| Ситуация | HTTP | Сообщение (пример) |
| --- | --- | --- |
| Нет входа | 401 | unauthorized |
| Нет доступа к туннелю / функции | 403 | forbidden |
| Событие не найдено | 404 | event not found |
| Событие не пригодно для повтора | 400 | event is not replayable |
| Сохранённое тело запроса обрезано, а overrides.body не указано | 400 | stored request body was truncated; provide a body override to replay |
| Событие обрезано политикой захвата (уровень метаданных) | 400 | cannot replay truncated event |
| Недопустимый путь или заголовок в overrides | 400 | описание валидации |
| Туннель не активен или клиент не подключён | 400 / 503 | tunnel is not active / tunnel client is not connected |
| Превышен лимит запросов | 429 | rate limit exceeded |
| Цель недоступна (таймаут, отказ) | 502 / 504 | понятная подсказка шлюза |
| Внутренняя ошибка | 500 | internal server error |
Ответ 4xx или 5xx от целевого сервиса при успешной доставке возвращается в поле downstream_status с кодом 200 у API повтора.
Ограничения
Известные ограничения
- Повторяются только HTTP/HTTPS-события, помеченные как пригодные для повтора.
- Тело запроса при повторе: если на тарифе включён захват тел и тело сохранено без обрезки — подставляется автоматически; если тело не сохранялось (только метаданные) — нужно указать
overrides.bodyили поле в форме повтора; если тело было обрезано лимитом захвата — автоматический повтор блокируется до явной подстановки тела. - Назначение фиксировано: целевой адрес туннеля из события, без выбора другого хоста.
- Заголовки авторизации, cookies, Host и hop-by-hop не передаются и не могут быть добавлены через overrides.
- Для повтора клиент туннеля должен быть подключён; иначе доставка невозможна.
- Действует лимит частоты запросов и ограничение по времени ожидания ответа цели.
Дополнения
Compare attempts & bulk / alternate replay
Общее описание функционала
Сравнение и массовый повтор дополняют базовый Replay в инспекторе трафика.
Пользователь может сопоставить два сохранённых события и увидеть различия в методе, адресе, заголовках, теле, статусе и длительности — без раскрытия секретов в теле, query или заголовках. Можно повторить выбранную группу событий одной операцией и отправить запросы на текущую цель туннеля, на порт этой же цели или на заранее разрешённый HTTPS-адрес облачного inbox. Для каждой попытки сервис сохраняет отдельный результат в инспекторе и связывает его с исходным событием.
Одиночный повтор через форму события остаётся совместимым: в ответе event_id всегда указывает на исходное событие, а поля попытки (attempt_event_id, attempt_number, root_event_id) добавляются рядом.
Как пользователь может использовать
- Откройте туннель → Инспектор → События, выберите два события и нажмите Сравнить, чтобы проверить, что изменилось между двумя вызовами вебхука.
- В диалоге сравнения читайте пометки у тела (
missing,invalid_json,text,truncated,unavailable,masked_full,available) — они описывают доступность данных, а не ошибку API. - Выберите несколько событий и запустите Массовый повтор, если нужно прогнать один и тот же обработчик на всей выборке.
- Задайте режим Только ошибочные или временной диапазон, чтобы повторить подходящую группу без ручного отбора.
- Оставьте назначение Текущая цель туннеля, если запрос должен идти на адрес, настроенный для туннеля.
- Откройте Другой адрес и укажите Порт цели, если нужен другой порт на той же принадлежащей вам цели (хост и схема задаются туннелем, не вводятся вручную).
- Выберите Удалённый HTTPS, если запрос должен попасть на разрешённый HTTPS-адрес под самым длинным префиксом inbox, который принадлежит вам; query, учётные данные и fragment в адресе не допускаются.
- Следите за счётчиками в карточке задания; после завершения откройте ссылку на попытку, чтобы увидеть статус и вернуться к исходному событию.
- Нажмите Отменить: ещё не отправленные элементы станут
cancelled; для запроса, который уже ушёл, но итог не подтверждён, результат будетunknown; завершённые попытки не отзываются. - Используйте стабильный заголовок
X-Fortunnels-Replay-IDна стороне получателя, если нужна дедупликация при повторной доставке.
Массовый повтор работает по модели «не менее одного раза»: после сбоя процесса один и тот же запрос может быть доставлен повторно с тем же идентификатором повтора. Сервис не обещает доставку «ровно один раз».
Как это реализовано в сервисе
Сервис проверяет право пользователя видеть туннель и каждое событие, затем фиксирует состав массового задания. Для каждого элемента создаётся отдельная попытка с неизменяемой ссылкой на исходное событие. Результат попытки отображается в инспекторе; исходное событие не изменяется.
При сравнении сервис возвращает только защищённые представления тел и очищенные заголовки. Секреты из тела, query и чувствительных заголовков не попадают в ответ сравнения. Перед отправкой на альтернативный адрес проверяются тарифные возможности, принадлежность адреса и правила безопасного соединения; при отказе сервис не раскрывает, существует ли чужой объект. Задания можно отменять; незавершённые элементы возвращаются в работу после истечения срока ожидания.
Сравнение событий
POST /api/webhook-replay/compare
Сравнивает два события, доступных текущему пользователю. Требуется авторизация, функция «Webhooks & Replay» с дополнением сравнения и массового повтора, CSRF-токен для запросов из браузера.
Тело запроса:
{
"left_event_id": "<uuid>",
"right_event_id": "<uuid>"
}
Успешный ответ 200:
| Поле | Описание |
| --- | --- |
| left_event_id, right_event_id | Сравниваемые события |
| truncated | true, если хотя бы одна сторона ограничена по размеру при сравнении |
| diffs | Список различий по полям |
Элемент diffs:
| Поле | Описание |
| --- | --- |
| field | Имя поля (method, path, query, header:…, request_body, response_status, response_body, duration_ms и др.) |
| left, right | Защищённые значения сторон (маскированные для тел и чувствительных заголовков) |
| equal | Совпадают ли значения |
| kind | scalar, header, json или text |
| left_note, right_note | Пометка доступности тела на каждой стороне (см. ниже) |
Пометки тела (left_note / right_note) — данные ответа, не HTTP-ошибка:
| Пометка | Смысл |
| --- | --- |
| available | Тело доступно для сравнения |
| missing | Тело не было сохранено |
| unavailable | Тело не удалось прочитать из хранилища |
| masked_full | Тело доступно только в защищённом виде |
| truncated | Сохранённое или сравниваемое тело обрезано по лимиту |
| invalid_json | Тело не является валидным JSON |
| text | Тело сравняется как текст, не как JSON |
Секреты из тела, query и чувствительных заголовков не возвращаются в ответе сравнения.
Одиночный повтор (совместимость)
POST /api/v1/inspector/events/{eventId}/replay
Поведение базового Replay сохранено. В успешном ответе 200 поле event_id всегда указывает на исходное событие. Дополнительные поля попытки:
| Поле | Описание |
| --- | --- |
| attempt_event_id | Идентификатор нового события попытки в инспекторе |
| attempt_number | Порядковый номер попытки для корневого события |
| root_event_id | Идентификатор корневого (исходного) события |
Полный контракт одиночного повтора — в документации Webhooks & Replay.
Массовый повтор
POST /api/webhook-replay/jobs
Создаёт задание массового повтора. Требуется авторизация, доступ к туннелю и функция «Webhooks & Replay» с дополнением массового повтора. Для запросов из браузера также требуется CSRF-токен.
Общие поля:
| Поле | Описание |
| --- | --- |
| tunnel_id | Идентификатор туннеля пользователя |
| selector_mode | selected, failed_only или time_range |
| event_ids | Список событий для режима selected |
| from, to | Границы времени в формате RFC 3339 для режима time_range |
| destination_kind | current, local_port или remote_https |
| local_port | Порт от 1 до 65535 для local_port (только порт; хост и схема берутся из цели туннеля) |
| remote_url | HTTPS-адрес без query, учётных данных и fragment для remote_https; путь должен находиться под самым длинным префиксом inbox, принадлежащим вызывающему |
Назначения:
| destination_kind | Поведение |
| --- | --- |
| current | Целевой адрес туннеля, как при обычном Replay |
| local_port | Тот же хост и схема, что у цели туннеля, но указанный порт; произвольный хост не допускается |
| remote_https | Разрешённый HTTPS-адрес облачного inbox; перенаправления не выполняются; для тарифа нужен доступ к удалённой пересылке |
За одно задание принимается не более 500 событий. Для одного пользователя одновременно может быть не более пяти активных заданий.
Ответ 200 содержит объект job с идентификатором, состоянием, режимом выбора, назначением, счётчиками total_count, succeeded_count, failed_count, skipped_count, cancelled_count, unknown_count и пояснением о доставке «не менее одного раза`.
Состояние и отмена
GET /api/webhook-replay/jobs/{jobId}
Возвращает состояние и счётчики задания. Терминальные состояния задания: completed, completed_with_errors, cancelled.
POST /api/webhook-replay/jobs/{jobId}/cancel
Запрашивает отмену задания. Элементы, которые ещё не отправлены, переходят в cancelled. Запрос, который уже ушёл, но итог доставки не подтверждён, обозначается как unknown. Завершённые попытки сохраняются.
Список заданий
GET /api/webhook-replay/jobs?tunnel_id=<tunnelId>
Возвращает задания указанного туннеля текущего пользователя.
Каждая завершённая попытка доступна через ссылку в инспекторе и содержит стабильный идентификатор X-Fortunnels-Replay-ID для дедупликации на стороне получателя.
Ошибки сравнения и массового повтора
| Ситуация | HTTP | Что сделать |
| --- | --- | --- |
| Не выполнен вход | 401 | Войдите в аккаунт и повторите запрос. |
| Функция недоступна по тарифу или аккаунт приостановлен | 403 | Проверьте тариф и состояние аккаунта. Для удалённой HTTPS-цели включите доступ к удалённой пересылке inbox. |
| Событие, туннель, задание или удалённый адрес недоступны текущему пользователю | 404 | Проверьте идентификатор и права доступа. Сервис намеренно не раскрывает существование чужих объектов — ответ не отличается от «не найдено». |
| Неверный режим выбора, UUID, порт или адрес назначения | 400 | Исправьте поля запроса. Для local_port передавайте только порт. Для remote_https используйте разрешённый HTTPS-адрес без query, учётных данных и fragment, под вашим префиксом inbox. |
| Сохранённое тело обрезано | 400 | Укажите новое тело явно в форме повтора или в overrides.body. |
| Слишком много событий или активных заданий | 400 или 409 | Уменьшите выборку (лимит 500 событий на задание, 5 активных заданий на пользователя), дождитесь завершения активного задания и повторите операцию. |
| Превышен лимит запросов | 429 | Подождите и повторите позже. |
| Цель не отвечает или отклонила соединение | 502 или 504 | Проверьте доступность цели туннеля или удалённого inbox. Запись о неудачной попытке останется в задании. |
| Запрос отменён во время доставки | 200 | Откройте результат элемента: он может быть cancelled или unknown, если итог доставки нельзя подтвердить. |
| Внутренняя ошибка сервиса | 500 | Повторите операцию. Если ошибка сохраняется, обратитесь в поддержку. |
HTTP-ответ целевого сервиса не означает ошибку API повтора: код цели записывается в результате попытки и доступен в инспекторе.
Пометки тела при сравнении (не ошибки)
Поля left_note и right_note в ответе POST /api/webhook-replay/compare описывают доступность тела на каждой стороне. Они не сопоставляются с HTTP-кодами ошибки.
| Пометка | Когда появляется | Что ожидать |
| --- | --- | --- |
| missing | Тело не было сохранено для события | Сравнение по телу ограничено; секреты не раскрываются |
| unavailable | Тело не удалось прочитать | Повторите сравнение позже; при постоянной ошибке обратитесь в поддержку |
| masked_full | Доступно только защищённое представление | Различия видны в маскированном виде |
| truncated | Тело обрезано по лимиту сравнения | Полное содержимое может быть недоступно в сравнении |
| invalid_json | Тело не является валидным JSON | Сравнение выполняется как текст, не как структурированный JSON |
| text | Тело сравняется как текст | Нормальный режим для не-JSON содержимого |
| available | Тело доступно для сравнения | Обычный случай при сохранённом теле |
Известные ограничения
- За одно массовое задание можно выбрать не более 500 событий; одновременно у пользователя может быть не более пяти активных заданий.
- Выборки
failed_onlyиtime_rangeфиксируются при создании задания. Новые события не добавляются в уже созданное задание. - Сервис доставляет запросы не менее одного раза. При сбое после отправки возможна повторная доставка с тем же
X-Fortunnels-Replay-ID. Доставка «ровно один раз» не гарантируется. - Событие с обрезанным телом нельзя повторить автоматически без явного нового тела.
- Сравнение ограничивает размер каждой стороны. Большие тела показываются с пометкой
truncated. - Пометки тела при сравнении (
missing,unavailable,masked_full,truncated,invalid_json,text,available) различаются и не объединяются в одну категорию. - Секреты из тела, query и чувствительных заголовков не предназначены для просмотра через сравнение; в ответе только защищённые представления.
- Для
local_portхост и схема берутся из принадлежащей пользователю цели туннеля; указать произвольный хост нельзя — только порт. - Удалённая цель (
remote_https) должна быть разрешена для пользователя, использовать HTTPS, находиться под самым длинным принадлежащим префиксом inbox и пройти проверки безопасного соединения. Query, учётные данные и fragment в адресе не допускаются. Перенаправления не выполняются. - При отказе по правам или принадлежности удалённого адреса сервис отвечает как при «не найдено», без раскрытия существования чужого объекта.
- Отмена не отзывает запрос, который уже был отправлен. Его итог сохраняется как доставленный, ошибочный или
unknown, если результат доставки не подтверждён. Ещё не отправленные элементы переходят вcancelled. - Одиночный повтор сохраняет совместимость:
event_idв ответе всегда указывает на исходное событие; поля попытки (attempt_event_id,attempt_number,root_event_id) добавляются отдельно.