Что такое webhook и как принять вебхук на Python

Ноутбук с двумя окнами терминала, в одном из них появляются строки входящих запросов Технологии

Webhook (вебхук) это HTTP-запрос, который сервис сам отправляет на указанный вами URL, когда у него происходит событие: пришла оплата, в репозиторий запушили коммит, боту написали сообщение. Вместо того чтобы раз за разом запрашивать API «есть что-то новое?», приложение один раз сообщает сервису адрес, и данные о событии приходят почти в реальном времени.

Что такое вебхук простыми словами

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

Отправитель это система, где происходит событие (GitHub, платёжная система, Telegram), получатель это ваш обработчик. Данные о событии отправитель кладёт в тело запроса, чаще всего в формате JSON.

Пример из интернет-магазина. Покупатель оплатил заказ, банк подтвердил платёж, и платёжная система автоматически отправляет уведомление на URL магазина. Магазин меняет статус заказа на «оплачен» и ставит задачу на сборку. Без вебхука он узнал бы об оплате, только когда сам запросит API платёжной системы.

Как работает вебхук

  1. Вы создаёте вебхук в настройках сервиса или с помощью его API: указываете URL и выбираете события, о которых нужны уведомления.
  2. При наступлении события сервис формирует POST-запрос, тело которого содержит данные события.
  3. Приложение получателя принимает данные, проверяет, что запрос отправил нужный источник, и отвечает кодом 2xx, например 200 OK.
  4. Если ответа нет или код другой, доставка считается неудачной. Что дальше, зависит от сервиса: один повторит отправку сам, другой оставит это вам.

Например, при пуше в репозиторий GitHub отправляет заголовки X-GitHub-Event с именем события (push), X-GitHub-Delivery с уникальным id доставки и X-Hub-Signature-256 с подписью тела, если у вебхука задан секрет.

Что такое webhook URL

Это адрес, на который система будет отправлять уведомления о событиях, например https://example.com/webhook, где /webhook путь к обработчику. Он должен быть доступен из интернета: localhost на вашем ноутбуке сервис не увидит. Stripe принимает для регистрации только публичный адрес с HTTPS, Telegram отправляет обновления HTTPS-запросом и только на порты 443, 80, 88 или 8443.

Чем вебхук отличается от API и опроса

Вебхук не замена API, а другой механизм получения новых данных. API (application programming interface, интерфейс программирования приложений) отвечает, когда клиент его спросил. Без вебхука клиент сам опрашивает сервер (polling): отправляет запрос раз в N секунд и сравнивает ответ с прошлым. Например, опрос раз в 10 секунд даёт 86 400 / 10 = 8640 запросов в сутки на один ресурс, даже если событий не было ни одного. Если ресурсов сотни, количество запросов растёт, и можно быстро упереться в лимит API. Вебхук приходит, только когда событие произошло, почти в реальном времени.

Читайте также:  REST API

Слева пустой почтовый ящик с протоптанной к нему тропинкой, справа письмо у двери

Опрос API (polling) Вебхук WebSocket
Кто начинает обмен клиент сервис-отправитель клиент открывает соединение
Соединение новый запрос на каждый опрос новый запрос на каждое событие одно постоянное, данные идут в обе стороны
Нужен публичный адрес у получателя нет да нет

Оба подхода в одном сервисе есть у Telegram. Бот получает обновления либо опросом через метод getUpdates, либо вебхуком, заданным через setWebhook, и эти способы взаимоисключающие: пока вебхук установлен, getUpdates не работает.

После вебхука часто всё равно идут в API: уведомление сообщает о событии, а полную информацию или следующее действие приложение получает обычным запросом. Как устроены такие запросы, мы разбирали в статье про REST API.

Пример: приём вебхука на локальном сервере Python

Напишем приложение, которое принимает вебхуки в формате GitHub. Хватит стандартной библиотеки Python 3.14. Сервер читает сырое тело, проверяет подпись, отсеивает повторную доставку, сразу отвечает, а данные передаёт в очередь.

Конверт с печатью проходит через рамку проверки и ложится в лоток очереди

# receiver.py: приёмник вебхуков, Python 3.14, только стандартная библиотека
import hashlib, hmac, json, os, queue, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRET = os.environ.get("WEBHOOK_SECRET", "dev-secret").encode()
seen, lock = set(), threading.Lock()  # id принятых доставок
jobs = queue.Queue()  # очередь событий на обработку


def valid(raw, header):
    if not header:
        return False
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), header.encode())


def worker():
    while True:
        event, data = jobs.get()
        print(f"обработка: {event}, ветка {data.get('ref')}", flush=True)


class Hook(BaseHTTPRequestHandler):
    def reply(self, code, text):
        self.send_response(code)
        self.send_header("Content-Length", str(len(text)))
        self.end_headers()
        self.wfile.write(text.encode())
        print(f"ответ {code}: {text}", flush=True)

    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        if not valid(raw, self.headers.get("X-Hub-Signature-256")):
            return self.reply(401, "bad signature")
        delivery = self.headers.get("X-GitHub-Delivery")
        if not delivery:
            return self.reply(400, "no delivery id")
        try:
            data = json.loads(raw)
        except ValueError:
            data = None
        if not isinstance(data, dict):
            return self.reply(400, "not a JSON object")
        if self.headers.get("X-GitHub-Event") != "push":
            return self.reply(202, "ignored")  # другие события пример не обрабатывает
        with lock:  # id запоминаем только для принятой доставки
            repeat = delivery in seen
            seen.add(delivery)
        if repeat:
            return self.reply(200, "duplicate")
        jobs.put((self.headers.get("X-GitHub-Event"), data))  # работу делает worker
        self.reply(202, "accepted")

    def log_message(self, *args):
        pass  # вместо стандартного журнала печатаем свой


threading.Thread(target=worker, daemon=True).start()
print("Жду вебхуки на http://127.0.0.1:8000/webhook", flush=True)
ThreadingHTTPServer(("127.0.0.1", 8000), Hook).serve_forever()

Подпись проверяется по байтам тела до json.loads. Если разобрать JSON и собрать обратно, байты изменятся: json.dumps(json.loads(b'{"ref":"refs/heads/dev"}')) вернёт {"ref": "refs/heads/dev"} с пробелом после двоеточия, и подпись не сойдётся. Работу выполняет отдельный поток worker, а обработчик кладёт событие в очередь и сразу отвечает 202: GitHub ждёт код 2xx не дольше 10 секунд, потом считает доставку неудачной. Доставка без X-GitHub-Delivery и тело, которое не JSON-объект (массив json.loads разберёт, но data.get на нём упадёт), получают 400, а id запоминается только после разбора. Всё, кроме push, получает 202 ignored без обработки.

Запустите сервер в первом окне терминала (в дистрибутивах, где команды python нет, запускайте python3):

python receiver.py
Жду вебхуки на http://127.0.0.1:8000/webhook

Как отправить тестовый вебхук и проверить подпись

Во втором окне отправим запросы через curl так, как их отправляет GitHub. Команды проверены в Git Bash на Windows, в Linux и macOS мы их не запускали. Подпись считает openssl dgst -hmac, флаг --json есть в curl с версии 7.82.0, -H @файл берёт заголовки из файла.

URL=http://127.0.0.1:8000/webhook
BODY='{"ref":"refs/heads/main"}'
SIG=sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "dev-secret" | sed 's/^.* //')
hook() { curl -s -w ' %{http_code}\n' -H "X-GitHub-Event: ${EVENT:-push}" "$@" $URL; }
hook -H "X-GitHub-Delivery: d1" -H "X-Hub-Signature-256: $SIG" --json "$BODY"
hook -H "X-GitHub-Delivery: d1" -H "X-Hub-Signature-256: $SIG" --json "$BODY"
EVENT=ping hook -H "X-GitHub-Delivery: d3" -H "X-Hub-Signature-256: $SIG" --json "$BODY"
hook -H "X-GitHub-Delivery: d2" -H "X-Hub-Signature-256: $SIG" --json '{"ref":"refs/heads/prod"}'
hook -H "X-Hub-Signature-256: $SIG" --json "$BODY"
hook -H "X-Hub-Signature-256: sha256=0000" --json "$BODY"
hook --json "$BODY"
printf 'X-Hub-Signature-256: sha256=\xc3\xa9\r\n' > bad-header.txt
hook -H @bad-header.txt --json "$BODY"
accepted 202
duplicate 200
ignored 202
bad signature 401
no delivery id 400
bad signature 401
bad signature 401
bad signature 401

На повтор с тем же X-GitHub-Delivery сервер отвечает 200, но второй раз событие в работу не идёт, третий запрос, событие ping, подтверждён без обработки. Четвёртый несёт настоящую подпись от другого тела, и сервер его отверг. Пятый подписан верно, но без id доставки, он получил 400. Дальше 401 получили чужая подпись, запрос без подписи и подпись с буквой é. В окне сервера для принятой доставки появляется строка из очереди: обработка: push, ветка refs/heads/main.

Последний запрос нужен для ловушки. Если сравнивать строки, hmac.compare_digest(expected, header), как в примере GitHub, заголовок с не-ASCII буквой роняет обработчик: в консоли TypeError: comparing strings with non-ASCII characters is not supported, у curl curl: (52) Empty reply from server. Строки она принимает только из ASCII, поэтому receiver.py сравнивает байты. Заголовок взят из файла: из аргумента -H Git Bash передал é как e.

Лупа над сургучной печатью на конверте

Безопасность вебхуков: секрет и подпись

Адрес обработчика может узнать кто угодно и отправить на него выдуманное «оплачено». Проверка подлинности запросов закрывает эту дыру: GitHub и Stripe считают HMAC-SHA256 с общим секретом (GitHub по телу, Stripe по времени и телу) и кладут результат в заголовок, а вы считаете то же самое и сравниваете. Без секрета подпись не подделать, а любая правка тела её ломает. Чем HMAC отличается от обычного хеша, разобрано в статье про хеш-функции.

Читайте также:  OAuth 2.0: что это и как работает протокол авторизации

Свою функцию сверьте с примером GitHub, где даны секрет, тело и ожидаемый результат:

# github_vector.py: сверка с тестовым примером GitHub
import hashlib, hmac

sig = hmac.new(b"It's a Secret to Everybody", b"Hello, World!", hashlib.sha256)
print("sha256=" + sig.hexdigest())
print(sig.hexdigest() == "757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17")
sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
True

У каждого сервиса своя схема, и правила GitHub на другие системы не переносятся:

Сервис Заголовок Что в нём
GitHub X-Hub-Signature-256 sha256= и HMAC-SHA256 тела в hex
Stripe Stripe-Signature время t= и подпись v1= от строки «время, точка, тело»; библиотеки Stripe по умолчанию отвергают запрос, если метка времени в заголовке расходится с текущим временем больше чем на 5 минут
Telegram X-Telegram-Bot-Api-Secret-Token подписи нет, приходит сам секрет из параметра secret_token метода setWebhook

Подпись и секрет сравнивайте через hmac.compare_digest(): обычный == прерывает сравнение на первом несовпадении, и по времени ответа атакующий может подбирать подпись (атака по времени). compare_digest сравнивает без такого раннего выхода. Принимайте вебхуки по HTTPS, а секрет делайте длинной случайной строкой; мы берём, например, secrets.token_hex(32).

Повторы, таймауты и дубли

Сервис Сколько ждёт ответа Повторяет ли неудачную доставку
GitHub 2xx в течение 10 секунд нет, повтор вручную, с тем же X-GitHub-Delivery
Stripe 2xx нужно вернуть быстро, до сложной логики в боевом режиме до трёх дней с растущим интервалом
Telegram в описании Bot API срок не указан при коде не 2xx повторяет и после «разумного числа попыток» прекращает

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

Читайте также:  Что такое фреймворк

Лоток с несколькими одинаковыми конвертами, один выдвинут вперёд

Примеры использования вебхуков

  • Разработка и CI/CD: пуш в главную ветку GitLab запускает скрипт деплоя, чат команды получает оповещения о завершении пайплайна, внешние трекеры задач обновляются, системы мониторинга следят за активностью в репозиториях.
  • Платежи: платёжная система отправляет интернет-магазину уведомления об успешной оплате, например событие payment_intent.succeeded в Stripe.
  • Чат-боты: Telegram отправляет каждое новое обновление боту на webhook URL в виде JSON.
  • Автоматизация без кода: в n8n узел Webhook принимает данные от приложений и сервисов, когда у них происходит событие.

Ограничения вебхуков

Получателю нужен публичный адрес. Для отладки на своём компьютере разработчики используют инструменты, которые пересылают запросы на localhost: для GitHub это smee.io, для Stripe ngrok или Stripe CLI.

Ноутбук на столе соединён длинным кабелем с серверной стойкой в глубине комнаты

Если обработчик недоступен, данные можно потерять: GitHub неудачную доставку сам не повторяет, её доставляют вручную после восстановления сервера, Telegram сдаётся после «разумного числа попыток» и хранит обновления не дольше 24 часов. Очередь в нашем примере живёт в памяти и пропадает при перезапуске, в проекте её заменяют брокером сообщений или таблицей в базе данных.

Объём данных ограничен: у GitHub 25 МБ на доставку. Stripe считает редирект неудачной доставкой, поэтому указывайте конечный адрес. Модуль http.server не рекомендуется для продакшена и выполняет только базовые проверки безопасности. Рабочий приёмник пишут на фреймворке вроде Flask, FastAPI или Django, логика проверки подписи остаётся той же.

Опрос API лучше использовать, когда у приложения нет публичного адреса или сервис не предоставляет вебхуки. В остальных случаях выигрывает вебхук: опрос раз в 10 секунд тратит 8640 запросов в сутки, даже если событий не было.

Частые вопросы о вебхуках

Чем вебхук отличается от API?

Разница в том, кто начинает обмен: API отвечает на запрос клиента, вебхук приходит сам, когда произошло событие. Их часто используют вместе: за подробностями после вебхука идут в API.

Как проверить, что вебхук работает?

Поднимите локальный приёмник, как в примере выше, и проверьте через curl правильной и поддельной подписью. Затем пробросьте его наружу и отправьте настоящее событие из сервиса.

Что делать, если вебхук не приходит?

Откройте журнал доставок у отправителя: у Stripe это вкладка Event deliveries с кодом ответа по каждой попытке, у Telegram метод getWebhookInfo с последней ошибкой доставки. Затем проверьте, что адрес открывается из интернета, обработчик отвечает 2xx и успевает ответить, а сервер не отдаёт редирект.

Вебхук и redirect URI в OAuth это одно и то же?

Нет. В OAuth 2.0 сервер авторизации перенаправляет браузер пользователя на redirect URI, а вебхук сервис отправляет прямо на ваш сервер, браузер в этом не участвует.

Оцените статью
bestprogrammer.ru
Добавить комментарий