GraphQL и вебхуки
Зачем:GraphQL отдаёт ровно те поля, которые попросил клиент, а вебхук позволяет сервису самому сообщить вам о событии. После урока вы знаете, чем GraphQL отличается от REST, и настраиваете уведомление о заявке с проверкой подписи и защитой от повторов.
25 минут
Озвучено синтезом речи (Qwen3-TTS), голос не принадлежит человеку.
Вебхук — HTTP-запрос, которым сервис сам сообщает о событии на заданный адрес. GraphQL — API с одним адресом, где клиент сам перечисляет нужные поля.
Когда сервер сам стучится к вам
Представьте, что вам должен прийти платёж или заявка с формы. Как узнать об этом? Есть два способа. Можно спрашивать: каждые несколько минут обращаться к сервису «ну что, пришло?», и в девяти случаях из десяти слышать «нет». Это называется опросом, он расточителен и всегда запаздывает. А можно попросить сервис позвонить самому: «когда придёт, пришли мне сообщение на такой-то адрес». Такой обратный вызов по HTTP называется вебхуком (webhook, «веб-крючок»).
Работает он так. Вы сообщаете сервису адрес своего обработчика (https://example.ru/hooks/request). Когда событие случается (оплата, заявка, новый коммит), сервис сам отправляет на этот адрес запрос POST с описанием события в JSON. Ваш обработчик читает его, делает дело (например, пересылает заявку владельцу) и отвечает кодом 200. На том же принципе строятся уведомления об оплате, сообщения в мессенджерах и запуск сборки сайта после правки.
Три правила хорошего обработчика, и каждое родилось из чьей-то беды.
Правило первое: проверяйте, что запрос настоящий
Адрес вашего обработчика не секрет: его может узнать и отправить на него что угодно любой. Если обработчик верит первому попавшемуся запросу, то «заявкой» или «оплатой» можно притвориться. Поэтому сервис подписывает запрос. Самый распространённый способ называется HMAC: сервис берёт тело запроса и общий с вами секрет, считает из них хеш (контрольную строку) и присылает её в заголовке. Обработчик считает такую же сам и сравнивает. Совпало: запрос прислал тот, кто знает секрет, и тело не менялось по дороге. Не совпало: ответ 401 и больше ничего.
Подпись считают по сырому телу, ровно в том виде, в каком оно пришло (если сначала разобрать JSON и записать его заново, пробелы изменятся и подпись не сойдётся). Сравнивать подписи надо специальной функцией (timingSafeEqual), которая работает одинаковое время: обычное сравнение строк по скорости ответа подсказывает атакующему, сколько первых символов он угадал. Некоторые сервисы (например, Telegram) вместо подписи присылают в заголовке сам секрет, который вы задали при настройке, и тогда сравнивается он.
Проверьте руками: измените тело или секрет и посмотрите, как ведёт себя подпись. Браузер считает её настоящей криптографией, но ничего никуда не отправляет:
X-Signature: …
ожидает: … пришло: …
401: отклонено, подпись не сошлась. Либо тело изменили, либо запрос прислал тот, кто секрета не знает.
- Доставка сразу: событие обработано, но ответа сервис не дождался и повторит
- Доставка через 1 минуту: событие обработано, но ответа сервис не дождался и повторит
- Доставка через 5 минут: событие обработано, но ответа сервис не дождался и повторит
- Доставка через 30 минут: наконец ответ получен
Владелец получит сообщений: 1. Повторы того же события опознаются по id и игнорируются.
Расписание повторов условное: у каждого сервиса своё, обычно с нарастающими паузами на часы и сутки.
А секрет живёт не в коде. Всё, что попало в публичный репозиторий, считается утёкшим навсегда, поэтому секрет кладут в переменные окружения сервера (process.env.WEBHOOK_SECRET) или в хранилище секретов, а если он мог утечь, меняют.
Правило второе: отвечайте быстро
Сервис ждёт ответа недолго, секунды. Если вы за это время не ответили 2xx, он решает, что запрос не дошёл. Поэтому в обработчике сначала проверяют подпись, записывают событие и отвечают 200, а тяжёлую работу (отправить сообщение, пересчитать, вызвать другой сервис) делают после, например в очереди.
Правило третье: повтор не должен ничего испортить
Если ответа нет или он пришёл с ошибкой, сервис повторит запрос: через минуту, через пять, через час, с нарастающими паузами. Значит, доставка работает по принципу «хотя бы один раз», а не «ровно один». Один и тот же запрос может прийти дважды: например, вы обработали событие, но ответ потерялся по дороге.
Поэтому обработка должна быть идемпотентной: повторный запрос не должен давать повторного эффекта. Для этого у каждого события есть номер (id). Обработчик помнит, какие номера уже видел, и повторное событие просто подтверждает (200), не делая ничего. Без этого клиент получит две заявки, а покупатель заплатит дважды.
Проверьте себя: почему обработчик, получив повтор события, всё равно отвечает 200?
Ответ «200» говорит сервису «принято, больше не присылай». Если ответить ошибкой, сервис будет повторять дальше. А событие уже обработано, поэтому повторять работу не нужно.
Куда отправить заявку
Пересылать заявку владельцу можно разными путями: письмом на почту, сообщением в мессенджер, записью в таблицу. Для Telegram есть бот-API: обработчик отправляет запрос на sendMessage с токеном бота и текстом. Токен это такой же секрет, как и подпись: в коде его быть не должно.
Здесь есть и юридическая сторона. Имя и телефон посетителя персональные данные (152-ФЗ), а сообщение в мессенджер уходит на серверы за пределами вашего хостинга, а возможно, и страны. Каждый такой канал надо отразить в политике обработки персональных данных, и это стоит обсудить с юристом. О законе мы поговорим в части про качество и право.
GraphQL: клиент выбирает поля сам
Параллельно с REST появился другой способ спрашивать у сервера данные: GraphQL. Его придумали во Facebook в 2012 году (внутри компании, для мобильного приложения), а открыли в 2015. Отличие в том, что у API один адрес (POST /graphql), а клиент в теле запроса сам перечисляет, какие поля ему нужны:
{
items {
title
price
}
} и получает ровно их, ни больше ни меньше. В REST адрес /items отдаёт каждое предложение целиком, даже если странице нужно только название и цена. Если нужны данные нескольких связанных ресурсов, REST требует нескольких запросов, а GraphQL один. Структура данных описана схемой с типами, и по ней строятся документация и проверки.
Выберите поля и сравните размеры ответа:
{
items {
title
price
}
}{
"data": {
"items": [
{
"title": "Первое предложение",
"price": "0 000 ₽"
},
{
"title": "Второе предложение",
"price": ""
}
]
}
}REST по адресу GET /items отдал бы всё: 258 байт. GraphQL сэкономил 43% ответа: странице не нужны остальные поля. Цена GraphQL: отдельный слой на сервере, сложнее кэширование и защита от слишком тяжёлых запросов.
Цена у GraphQL тоже есть: нужен отдельный слой на сервере, кэшировать труднее (все запросы идут на один адрес), и надо защищаться от слишком тяжёлых запросов («дай всё про всех»). Поэтому его берут, когда у проекта несколько разных клиентов (сайт, мобильное приложение), которым нужны разные поля, и много связанных данных. А для списка из десятка услуг на сайте-визитке REST проще и достаточен.
Практика
Вы на сайте эпохи 2022 года, где сайты общаются с сервисами. Настройте путь заявки из формы до владельца. Настоящий приёмник в песочнице не запустить, и код обработчика вы напишете текстом, как он лежал бы на сервере, по образцу из уроков про подпись и повторы: в блоке pre на странице. Проверки найдут в нём главное: секрет из окружения, подпись HMAC, безопасное сравнение, ответ 401, защиту от повторов и быстрый ответ.
В двух абзацах ответьте словами: что будет, если приёмник недоступен, и когда для ваших данных удобнее GraphQL, а когда REST. Назовите один из ваших пунктов из брифа, чтобы рассуждение было про ваш проект.
Что было не так раньше
Главным способом узнать о событии был опрос, а для обмена между сервисами служил SOAP и файлы, которые кто-то по расписанию клал на FTP. Когда вебхуки появились, их принимали без подписи («адрес же никто не знает»), секрет вшивали в код и выкладывали на GitHub, а ответ «200» отдавали только после тяжёлой работы, из-за чего сервисы слали повторы, и дубли заявок приходилось разгребать вручную.
Правильное осталось: обратный вызов вместо опроса, подпись, быстрый ответ и идемпотентность. А GraphQL так и не вытеснил REST: оба живут, потому что решают чуть разные задачи.
Практика
Готовлю вашу страницу…
Тест
Вопрос 1 из 6
Карточки
Эти карточки попадут в повторение, когда вы завершите урок.
Что такое вебхук?
Ответ
Обратный вызов по HTTP: когда в сервисе случилось событие (оплата, заявка, коммит), он сам отправляет POST на ваш адрес с описанием события, вместо того чтобы вы его опрашивали.
Как проверяют подлинность вебхука?
Ответ
Сервис подписывает сырое тело запроса общим секретом (HMAC SHA-256) и присылает подпись в заголовке. Приёмник считает свою подпись и сравнивает безопасной функцией. Не сошлись: 401, событие не обрабатывать.
Почему обработка вебхука должна быть идемпотентной?
Ответ
Доставка «хотя бы один раз»: при сбоях сервис повторяет запрос. Запоминайте id событий и не обрабатывайте повторно. Отвечайте 200 быстро, тяжёлую работу делайте в очереди.
Что такое GraphQL?
Ответ
Язык запросов к API (Facebook, 2015): один адрес, типизированная схема, клиент сам перечисляет нужные поля и связанные данные. Нет лишних полей и лишних запросов, зато сложнее кэширование и защита.
Когда GraphQL, а когда REST?
Ответ
REST: простые одинаковые запросы, кэш, небольшие сайты. GraphQL: много разных клиентов (сайт, мобильное приложение) и много связанных данных; нужна команда, готовая его поддерживать.
Источники
- GraphQL: Introduction to GraphQL CC BY, по-английски
- Telegram Bot API: setWebhook только ссылка, по-английски
- Stripe: Webhooks только ссылка, по-английски
- GitHub Docs: Validating webhook deliveries CC BY, по-английски
Дальше
Теперь вы знаете: GraphQL, вебхуки.