Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Как Google Apps Script незаметно ломает вебхуки Telegram-ботов

Дата публикации: 03-08-2026 11:41:30

Бот на Telegram Bot API + Google Apps Script проходил все мои тесты, но иногда молчал у реальных пользователей. Разбираюсь, почему Apps Script может отвечать 302-редиректом вместо 200 на вебхук — и почему это невозможно поймать через HTTP-клиент, который сам следует за редиректами. Читать далее

Основное содержимое страницы с новостью.

Недавно собирал Telegram-бота на связке Google Apps Script + Telegram Bot API — казалось бы, стандартная и хорошо описанная схема: webhook, doPost(e), никакого сервера держать не надо. Всё прошло тесты, бот отвечал, я был доволен — а потом бот стал молчать по ночам без видимой причины, и найти причину оказалось неожиданно непросто, потому что мои же инструменты для тестирования её маскировали.

Симптом

Бот работал отлично, когда я тестировал его сам — отправлял POST-запросы напрямую на .../exec через requests в Python, получал 200 ok, всё логично. Но когда реальный пользователь писал боту с телефона, сообщения иногда просто не доходили. Никакой закономерности на первый взгляд: то работает, то нет.

Ложный след

Первым делом заподозрил гонку состояний или проблему с повторной доставкой апдейтов — благо, дело было не на пустом месте: чуть раньше у бота уже был реальный баг с отсутствием идемпотентности, из-за которого один зависший ретрай Telegram превратился в ~300 дублей сообщений за ночь. Добавил проверку по update_id через CacheService — правильное исправление само по себе, но проблема с молчанием бота осталась.

Настоящая причина

Разгадка нашлась в getWebhookInfo — методе Telegram Bot API, который показывает статус вебхука, включая последнюю ошибку доставки:

{
  "url": "https://script.google.com/macros/s/AKfycb.../exec",
  "last_error_message": "Wrong response from the webhook: 302 Found"
}

Вот и всё объяснение. У Google Apps Script есть не очень широко известная особенность: адрес вида https://script.google.com/macros/s/{id}/exec, задеплоенный как Web App, может отвечать неаутентифицированному вызывающему (а Telegram именно такой — у него нет и не может быть Google-сессии) редиректом 302 на script.googleusercontent.com вместо прямого 200. Браузеры и большинство HTTP-клиентов эту редирекцию просто прозрачно проходят — а вот система доставки вебхуков Telegram редиректы не обрабатывает вообще. Она либо получает 200 сразу же, либо считает доставку неуспешной и повторяет попытку позже.

Почему я не увидел это раньше

Потому что я тестировал через requests.post(url, ...) в Python — а эта библиотека по умолчанию сама следует за редиректами, полностью прозрачно для вызывающего кода. Мои собственные проверки показывали стабильные 200 ok на каждый запрос, потому что requests незаметно доходил до финального адреса за меня. Проблема была видна только с точки зрения самого Telegram, у которого такой прозрачности нет.

Решение

Раз Google не отвечает надёжным 200 на входящие POST-запросы для неаутентифицированных вызывающих, самый чистый выход — вообще не полагаться на то, что Telegram сможет достучаться до Apps Script напрямую. Вместо doPost (webhook) — time-based триггер, который сам, раз в минуту, спрашивает Telegram: «есть что-то новое?» через getUpdates.

function pollTelegram() {
  var props = PropertiesService.getScriptProperties();
  var offset = Number(props.getProperty('TG_OFFSET') || '0');

  var resp = UrlFetchApp.fetch(
    API + '/getUpdates?offset=' + offset + '&timeout=0',
    { method: 'get', muteHttpExceptions: true }
  );
  var data = JSON.parse(resp.getContentText());
  if (!data.ok || !data.result.length) return;

  data.result.forEach(function (update) {
    offset = update.update_id + 1;
    // ...обработка апдейта...
  });

  props.setProperty('TG_OFFSET', String(offset));
}

ScriptApp.newTrigger('pollTelegram').timeBased().everyMinutes(1).create();

Исходящие запросы через UrlFetchApp (когда Apps Script сам является инициатором) работают надёжно — проблема именно во входящих запросах на неаутентифицированный /exec-эндпоинт. Поменяв направление связи, редирект просто перестаёт быть релевантным.

Бонусный баг, который я сам туда внёс

Первая версия pollTelegram вызывала UrlFetchApp.fetch с method: 'get' и параметрами в payload. Для GET-запросов payload в UrlFetchApp не превращается в query-параметры — их нужно вручную собирать в саму строку URL. В итоге offset тихо никогда не отправлялся, и каждый запуск заново вычитывал один и тот же бэклог вместо только новых сообщений. Не опасно (идемпотентность спасала от дублей), но неэффективно и сбивало с толку, пока не заметил.

Что унести с собой

Если интеграция полагается на то, что внешний сервис (Telegram, Stripe, GitHub — неважно) сможет достучаться POST-запросом до Apps Script, стоит проверять доставку тем же способом, каким её проверяет реальный отправитель — а не HTTP-клиентом, который сам решает за вас часть проблем. curl -sI без -L, или прямая проверка last_error_message в getWebhookInfo — самый быстрый способ поймать именно эту категорию бага.


Автор ведёт блог о разработке — КодБлог.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Telegram 2026 в России: взгляд разработчика ботов08.2607-08-2026
2Я дал AI-агенту доступ к своему Telegram. Вот что из этого получилось07.6418-08-2026
3Я сделал полностью локальный Telegram Wrapped после того, как у меня угнали фейк с тремя переписками06.8921-07-2026
4Как я сделал общие доски в Telegram, которые живут прямо в чате07.0731-07-2026
5Как понять, что tg-канал накручен и лучше на него не заливать?08.9605-08-2026
6Анатомия VDD: Как я нашел RCE в форке Telegram, а Google Security не увидел «ничего вредоносного»08.0811-08-2026
7Полтора месяца мы вели трафик в Telegram. Потом поменяли маршрут. Подробный разбор06.0907-08-2026
8Как я навайбкодил бота в MAX и получил 2000 пользователей в течение 12 часов за 0 рублей010.7616-08-2026
9Математика stop‑loss в Telegram Ads. Когда выключать безрезультатное объявление09.9310-08-2026
10Как я посадил WireMock.Net на gRPC стероиды07.6228-07-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 10.36. Источник: habr.com.