Lede: HTTPS-адрес, проверка запросов, быстрый ответ, очередь обработки и защита от повторной обработки обновлений.

Webhook для Telegram-бота: настройка, безопасность и обработка повторов
Webhook для Telegram-бота: настройка, безопасность и обработка повторов

Webhook для Telegram-бота — это HTTPS-endpoint, на который Telegram отправляет обновления при каждом событии: новом сообщении, нажатии кнопки, присоединении участника. Для настройки требуется действующий сертификат HTTPS, регистрация URL через метод setWebhook, проверка подлинности запросов с помощью secret_token, ответ сервера в течение установленного времени и защита от повторной обработки одинаковых update_id. Дополнительно нужна очередь для обработки задач, чтобы избежать таймаутов и не терять обновления при временных проблемах.

Как выбрать webhook или long polling

Telegram Bot API даёт два взаимоисключающих способа получения Update: getUpdates и webhook. Long polling подходит и для продакшена, если процесс бота запущен в одном экземпляре и публичный HTTPS-endpoint не нужен. Webhook удобен, когда уже есть публичный HTTPS-сервис, нужно масштабировать приём или интегрировать его с веб-приложением.

Пока установлен webhook, getUpdates не работает. Для перехода на polling сначала явно вызовите deleteWebhook и осознанно выберите drop_pending_updates. Вызов getUpdates сам webhook не удаляет.

Требования к HTTPS-endpoint

Telegram принимает только HTTPS-адреса с действующим сертификатом. Самоподписанные сертификаты поддерживаются, но их нужно передать в параметре certificate при вызове setWebhook. Для продуктовых систем проще использовать сертификат от Let's Encrypt или другого публичного центра сертификации.

По умолчанию разрешены порты 443, 80, 88 и 8443. Если ваш сервер слушает другой порт, Telegram не сможет доставить обновления. Проверьте конфигурацию веб-сервера и убедитесь, что выбранный порт открыт в файрволе.

URL должен содержать уникальный случайный путь, например https://example.com/telegram/incoming/7f3c-random-path. Не вставляйте токен бота в URL: адрес может попасть в access-логи, метрики и отчёты об ошибках. Для аутентификации запроса используйте отдельный secret_token.

Настройка webhook через setWebhook

Метод setWebhook регистрирует адрес для доставки обновлений. Минимальный вызов выглядит так:

POST https://api.telegram.org/bot<TOKEN>/setWebhook
Content-Type: application/json

{
  "url": "https://example.com/telegram/incoming/7f3c-random-path"
}

После успешного ответа Telegram начнёт отправлять POST-запросы на указанный адрес. Каждый запрос содержит JSON-объект Update с полями update_id, message, callback_query и другими, в зависимости от типа события.

Дополнительные параметры метода:

  • certificate — файл самоподписанного сертификата в формате PEM, если не используете публичный центр сертификации;
  • ip_address — фиксированный IP, если DNS-запись ещё не обновлена или вы хотите ускорить доставку;
  • max_connections — максимальное число одновременных HTTPS-соединений от Telegram (по умолчанию 40, можно задать от 1 до 100);
  • allowed_updates — явный список нужных типов; если параметр не передать, Bot API сохранит прежнюю настройку; пустой список означает все типы, кроме chat_member, message_reaction и message_reaction_count;
  • drop_pending_updates — если true, все накопленные обновления будут удалены при регистрации webhook;
  • secret_token — строка до 256 символов, которую Telegram передаёт в заголовке X-Telegram-Bot-Api-Secret-Token каждого запроса. Используйте этот параметр для проверки подлинности.

Пример с полным набором параметров:

POST https://api.telegram.org/bot<TOKEN>/setWebhook
Content-Type: application/json

{
  "url": "https://example.com/telegram/incoming/7f3c-random-path",
  "secret_token": "random-string-256-chars",
  "max_connections": 50,
  "allowed_updates": ["message", "edited_message", "callback_query", "pre_checkout_query"],
  "drop_pending_updates": false
}

После настройки проверьте статус командой getWebhookInfo:

GET https://api.telegram.org/bot<TOKEN>/getWebhookInfo

Ответ содержит поля url, has_custom_certificate, pending_update_count, last_error_date и last_error_message. Если pending_update_count растёт, значит ваш сервер не отвечает вовремя или возвращает ошибку.

Проверка подлинности запросов
Проверка подлинности запросов

Проверка подлинности запросов

Любой, кто знает адрес вашего webhook, может отправить POST-запрос с произвольным JSON. Без проверки подлинности злоумышленник сможет имитировать обновления и заставить бота выполнить нежелательные действия. Параметр secret_token решает эту проблему: Telegram передаёт заданную строку в заголовке X-Telegram-Bot-Api-Secret-Token, а ваш сервер сравнивает её с сохранённым значением.

Алгоритм проверки:

  1. Сгенерируйте случайную строку длиной 32–64 символа (буквы, цифры, дефисы, подчёркивания).
  2. Сохраните её в переменной окружения или конфигурационном файле.
  3. Передайте в secret_token при вызове setWebhook.
  4. В обработчике webhook извлеките заголовок X-Telegram-Bot-Api-Secret-Token.
  5. Сравните значение с сохранённым токеном. Если не совпадает, верните HTTP 403 и проигнорируйте тело запроса.

Пример на Python (Flask):

import os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET_TOKEN = os.environ.get("TELEGRAM_SECRET_TOKEN")
if not SECRET_TOKEN:
    raise RuntimeError("TELEGRAM_SECRET_TOKEN must be non-empty")

@app.route("/telegram/incoming/7f3c-random-path", methods=["POST"])
def webhook():
    if request.headers.get("X-Telegram-Bot-Api-Secret-Token") != SECRET_TOKEN:
        abort(403)
    
    update = request.get_json()
    # Обработка update
    return "", 204

Дополнительная мера — проверка источника по IP. Telegram публикует диапазоны адресов в документации, но они могут меняться. Полагаться только на IP небезопасно: используйте secret_token как основной механизм, а фильтрацию по IP — как дополнительный уровень защиты.

Быстрый ответ и асинхронная обработка

Telegram считает доставку успешной при любом ответе 2xx, а не только 200. При другом статусе Telegram повторяет запрос и после разумного числа попыток прекращает их. Поэтому нельзя отвечать 2xx до того, как Update надёжно сохранён в своей базе или очереди.

Правильная архитектура разделяет приём и обработку:

  1. Endpoint принимает POST-запрос, проверяет secret_token, извлекает update_id и тело обновления.
  2. В транзакции базы выполняется INSERT ... ON CONFLICT DO NOTHING в inbox с уникальным update_id.
  3. Только после commit endpoint возвращает HTTP 204; при ошибке хранилища возвращает 503, чтобы Telegram повторил доставку.
  4. Воркер забирает обновление из очереди и выполняет бизнес-логику: отправку сообщений, запросы к базе данных, вызовы внешних API.

Упрощённый пример на Flask и PostgreSQL:

import os
from flask import Flask, request, abort
from psycopg import connect

app = Flask(__name__)
SECRET_TOKEN = os.environ.get("TELEGRAM_SECRET_TOKEN")
if not SECRET_TOKEN:
    raise RuntimeError("TELEGRAM_SECRET_TOKEN must be non-empty")

@app.post("/telegram/incoming/7f3c-random-path")
def webhook():
    if request.headers.get("X-Telegram-Bot-Api-Secret-Token") != SECRET_TOKEN:
        abort(403)
    update = request.get_json(force=True)
    with connect(os.environ["DATABASE_URL"]) as db:
        db.execute(
            "INSERT INTO telegram_inbox(update_id, body, state) "
            "VALUES (%s, %s, 'pending') ON CONFLICT (update_id) DO NOTHING",
            (update["update_id"], update),
        )
    return "", 204

Отдельный диспетчер выбирает pending-строки через FOR UPDATE SKIP LOCKED, пытается поместить их в очередь и меняет статус только после успеха. Если enqueue упал, строка остаётся pending и попадает в следующую попытку. Воркер дополнительно защищает каждую бизнес-операцию её собственным уникальным ключом.

Защита от повторной обработки обновлений

Каждый Update содержит уникальный update_id, который Bot API прямо рекомендует для игнорирования повторов. Если endpoint не вернул статус 2xx, Telegram повторит тот же Update. Без дедупликации бот может дважды выполнить бизнес-действие.

Механизм защиты:

  1. Извлеките update_id из JSON.
  2. Атомарно вставьте update_id и тело в durable inbox с уникальным ограничением.
  3. Если строка уже есть, верните 204: повтор уже сохранён и не требует второй записи.
  4. Если commit не состоялся, верните 503; не ставьте отметку processed до успешного enqueue и бизнес-действия.

Для inbox нужно durable-хранилище с атомарной вставкой и уникальным ключом. Обычный словарь в памяти теряется при перезапуске. Схема EXISTS, затем SETEX и затем enqueue тоже неверна: первые два шага допускают гонку, а сбой между отметкой и enqueue теряет Update. Если выбран Redis, нужен атомарный Lua-скрипт или Redis Streams с consumer groups, подтверждением после обработки и восстановлением pending-записей.

Обработка ошибок и повторная доставка
Обработка ошибок и повторная доставка

Обработка ошибок и повторная доставка

Если endpoint вернул статус вне диапазона 2xx или не ответил вовремя, Telegram повторит запрос и после «разумного количества попыток» прекратит доставку. Точное расписание Bot API не обещает, поэтому нельзя закладываться на конкретные интервалы.

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

Проверяйте getWebhookInfo регулярно. Поля last_error_date и last_error_message покажут последнюю проблему. Если pending_update_count растёт, увеличьте производительность воркеров или масштабируйте инфраструктуру.

Для критичных сценариев настройте мониторинг: отправляйте метрику pending_update_count в Prometheus, Datadog или CloudWatch и настройте алерт при превышении порога. Так вы узнаете о проблеме до того, как пользователи начнут жаловаться.

Типичные ошибки при настройке webhook

Несколько ошибок встречаются регулярно:

  • Недействительный сертификат HTTPS. Telegram проверяет цепочку сертификатов. Убедитесь, что сертификат не истёк, содержит корректное имя домена и подписан доверенным центром. Проверить можно командой curl -v https://example.com/webhook/... или онлайн-инструментами вроде SSL Labs.
  • Неправильный порт. Если ваш сервер слушает порт 8080, а Telegram разрешает только 443, 80, 88 и 8443, обновления не будут доставлены. Настройте прокси (nginx, Caddy, Traefik) для проксирования с разрешённого порта на внутренний.
  • Отсутствие проверки secret_token. Без проверки ваш бот уязвим к подделке обновлений. Всегда сравнивайте заголовок X-Telegram-Bot-Api-Secret-Token с сохранённым значением.
  • Долгая обработка в endpoint. Если вы выполняете тяжёлые операции синхронно, Telegram не дождётся ответа и повторит доставку. Переместите обработку в очередь.
  • Повторная обработка обновлений. Без проверки update_id одно и то же событие будет обработано несколько раз. Сохраняйте идентификаторы в Redis или базе данных.
  • Одновременное использование getUpdates и webhook. Telegram не разрешает оба метода одновременно. Если вы вызвали setWebhook, long polling отключится автоматически.
  • Накопление обновлений после сбоя. Если бот был недоступен несколько часов, накопятся тысячи обновлений. Используйте drop_pending_updates: true при восстановлении, если старые сообщения не критичны.

Безопасность и ограничение доступа

Webhook — публичный endpoint, поэтому защита обязательна. Основные меры:

  • secret_token — первый и главный уровень защиты. Генерируйте длинную случайную строку и проверяйте её в каждом запросе.
  • Фильтрация по IP. Telegram публикует диапазоны адресов, с которых отправляются запросы. Настройте файрвол или веб-сервер для блокировки запросов с других адресов. Помните, что диапазоны могут меняться — проверяйте документацию периодически.
  • Rate limiting. Ограничьте число запросов с одного IP в единицу времени. Это защитит от DDoS, даже если злоумышленник узнает URL.
  • HTTPS обязателен. Не используйте HTTP даже для тестирования в локальной сети: Telegram не примет такой адрес. Для разработки используйте long polling или туннель (ngrok, localtunnel) с действующим HTTPS.
  • Не храните секреты в коде. Токен бота, secret_token и параметры подключения к базе должны быть в переменных окружения или секретном хранилище (AWS Secrets Manager, HashiCorp Vault).
Чек-лист настройки webhook
Чек-лист настройки webhook

Чек-лист настройки webhook

Перед запуском в production проверьте:

  1. HTTPS-сертификат действителен и доверен публичным центром сертификации (или корректно передан самоподписанный).
  2. Сервер слушает один из разрешённых портов: 443, 80, 88 или 8443.
  3. URL содержит уникальный путь для каждого бота.
  4. Параметр secret_token сгенерирован, сохранён в переменной окружения и передан в setWebhook.
  5. Endpoint проверяет заголовок X-Telegram-Bot-Api-Secret-Token и возвращает HTTP 403 при несовпадении.
  6. Endpoint возвращает 2xx сразу после commit Update в durable inbox, не ожидая бизнес-обработки.
  7. Обработка обновлений выполняется в очереди (Celery, Bull, AWS SQS или аналог).
  8. update_id проверяется на дублирование с помощью Redis, базы данных или кеша в памяти.
  9. Настроено логирование ошибок с записью update_id и стека вызовов.
  10. Настроен мониторинг pending_update_count с алертом при превышении порога.
  11. Файрвол ограничивает доступ к webhook по IP или настроен rate limiting.
  12. Проверен статус через getWebhookInfo: url совпадает, pending_update_count равен нулю, last_error_date отсутствует.

FAQ: частые вопросы о webhook

Можно ли использовать один webhook для нескольких ботов?

Технически можно направить несколько ботов на один домен с разными путями: https://example.com/webhook/bot1, https://example.com/webhook/bot2. Обработчик должен различать ботов по пути или токену в URL и использовать соответствующий secret_token для проверки. Не направляйте разные боты на один и тот же путь — вы не сможете определить источник обновления.

Что делать, если pending_update_count постоянно растёт?

Это значит, что ваш сервер не успевает обрабатывать обновления или возвращает ошибки. Проверьте логи на наличие исключений, увеличьте число воркеров в очереди, оптимизируйте запросы к базе данных, добавьте кеширование. Если старые обновления не критичны, вызовите setWebhook с параметром drop_pending_updates: true, чтобы сбросить очередь.

Как протестировать webhook локально?

Telegram требует публичный HTTPS-адрес, поэтому прямое подключение к localhost невозможно. Используйте туннель: ngrok (ngrok http 8000) или localtunnel (lt --port 8000) создают временный HTTPS-адрес, который перенаправляет запросы на локальный сервер. Полученный URL передайте в setWebhook. Альтернатива — разворачивайте тестовый сервер на облачной платформе (Heroku, Render, Fly.io) с бесплатным HTTPS.

Нужно ли удалять webhook перед обновлением кода?

Не обязательно. При rolling или blue-green deployment можно оставить webhook и положиться на повторную доставку. deleteWebhook нужен для явного перехода на getUpdates, а не как способ «остановить накопление». Параметр drop_pending_updates: true безвозвратно удаляет ожидающие Update, поэтому его включают только после осознанного решения отказаться от них.

Можно ли отправить Bot API-запрос прямо в ответе на webhook?

Да, официальный FAQ описывает возможность выполнения Bot API-запроса в ответе на webhook-обновление. Вместо обычного HTTP 200 вы возвращаете статус 200 с телом, содержащим JSON-объект метода (например, sendMessage). Telegram выполнит этот запрос от имени бота. Это работает для простых сценариев, но не заменяет очередь: если требуется несколько запросов или сложная логика, используйте асинхронную обработку.

Как часто Telegram повторяет доставку при ошибке?

Bot API не фиксирует точный алгоритм повторов и обещает лишь «разумное количество попыток». Проверяйте getWebhookInfo и мониторьте last_error_date, last_error_message и pending_update_count.

Практический вывод

Webhook требует HTTPS, setWebhook, непустого secret_token и явного allowed_updates. Endpoint атомарно записывает Update в durable inbox с уникальным update_id и лишь потом отвечает 2xx. Диспетчер не отмечает строку до успешного enqueue, а воркер делает бизнес-операции идемпотентными. Это закрывает гонку параллельных доставок и позволяет повторить сбой enqueue; абсолютную гарантию «ни одной потери и ни одного дубля» распределённая система не даёт, поэтому нужны мониторинг и разбор зависших строк.