Розробники часто стикаються з ситуацією, коли відправка сповіщень через Telegram Bot API починає «падати» або мовчки втрачати повідомлення. Поширена причина — спрощена реалізація через file_get_contents без перевірки HTTP-кодів або неекранований текст у режимі parse_mode: HTML, який призводить до помилки 400 Bad Request: can't parse entities.
У продакшн-оточенні отправка сообщений має бути надійним процесом. Для цього потрібен повноцінний cURL-клієнт, трирівнева валідація відповідей сервера та суворе екранування динамічних даних.
Чому file_get_contents не підходить для продакшну
Використання file_get_contents('https://api.telegram.org/bot...') створює три ключові проблеми:
- Відсутність гнучких таймаутів: Функція використовує системний
default_socket_timeout. Якщо Telegram або шлюз зависне, скрипт буде заблоковано на 60 секунд. - Складність збору HTTP-статусів: При відповідях
400 Bad Requestабо429 Too Many Requestsфункція повертаєfalseі генерує PHP Warning замість того, щоб віддати тіло відповіді з деталізацією помилки. - Відсутність точного контролю заголовків: Передача даних у форматі
application/x-www-form-urlencodedабоapplication/jsonвимагає громіздкого налаштування контексту черезstream_context_create.
Використання cURL дозволяє явно задати мережеві ліміти, отримати точний HTTP-код відповіді та прочитати JSON з помилкою від Telegram.
Трирівнева система перевірки відповіді Telegram API
Будь-який запит до api.telegram.org повинен проходити через три послідовних етапи перевірки:
- Мережевий рівень (cURL): Перевірка відсутності помилок сокета, таймаутів і проблем із SSL-сертифікатами (перевірка
curl_errno). - Синтаксичний рівень (JSON): Валідація структури відповіді за допомогою
json_decodeта контрольjson_last_error(). - Прикладний рівень (Telegram Bot API): Аналіз поля
okв отриманому масиві. Telegram може повернути HTTP 200 або 400, але якщоok === false, запит вважається необробленим.
Нижче наведено базову функцію-клієнт, що реалізує всі три рівні перевірок:
<?php
/**
* Отправка сообщения в Telegram с кастомной обработкой ошибок
*
* @param string $botToken Токен бота из getenv()
* @param int|string $chatId ID чата
* @param string $text Текст сообщения
* @param string $parseMode Режим разметки (HTML)
* @return array Массив result из ответа Telegram
* @throws RuntimeException При сетевых ошибках или некорректном ответе API
*/
function sendTelegramApiMessage(string $botToken, int|string $chatId, string $text, string $parseMode = 'HTML'): array
{
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$payload = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => $parseMode,
'disable_web_page_preview' => true,
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 7,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 1. Сетевая проверка
if ($curlErrno !== 0) {
throw new RuntimeException("cURL Network Error [{$curlErrno}]: {$curlError}");
}
// 2. Проверка парсера JSON
$data = json_decode((string)$response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("JSON Parse Error: " . json_last_error_msg() . " | Raw response: " . substr((string)$response, 0, 200));
}
// 3. Проверка статуса Telegram API
if (!isset($data['ok']) || $data['ok'] !== true) {
$errorCode = $data['error_code'] ?? $httpCode;
$description = $data['description'] ?? 'Unknown Telegram API Error';
// Дополнительно проверяем наличие Retry-After при лимите 429
$retryAfter = $data['parameters']['retry_after'] ?? null;
$suffix = $retryAfter ? " (Retry-After: {$retryAfter}s)" : "";
throw new RuntimeException("Telegram API Error [{$errorCode}]: {$description}{$suffix}");
}
return $data['result'];
}
Таймаути: CURLOPT_CONNECTTIMEOUT та CURLOPT_TIMEOUT
При відправці сповіщеньважливо розмежовувати два види таймаутів:
CURLOPT_CONNECTTIMEOUT: Час очікування попереднього TCP/TLS-з'єднання із серверами Telegram. Значення в 3–5 секунд більш ніж достатньо.CURLOPT_TIMEOUT: Повний час виконання запиту. Для синхронної відправки в HTTP-запиті вебсайту ставте 5–10 секунд, щоб скрипт не зависав при проблемах на магістральних каналах.
Якщо вам необхідно розсилати сотні повідомлень, не виконуйте їх послідовно всередині одного HTTP-запиту користувача. Зберігайте сповіщення в БД або чергу (Yii Queue, Laravel Queues) і обробляйте їх фоновим демоном.
Безпечний parse_mode HTML та екранування спецсимволів
Режим parse_mode: HTML дозволяє форматувати текст за допомогою обмеженого набору тегів: <b>, <i>, <code>, <pre>, <a href="...">. Однак будь-який неекранований символ <, > або & у користувацьких даних зруйнує HTML-дерево, і Telegram поверне помилку 400 Bad Request.
Для захисту шаблонів не можна застосовувати htmlspecialchars до всього підсумкового тексту повідомлення — це перетворить ваші теги <b> на <b>. Екранувати потрібно виключно динамічні підстановки (імена, коментарі, дані з форм).
<?php
/**
* Безопасное формирование HTML-сообщения по шаблону
*
* @param string $template Шаблон с плейсхолдерами, например: "Привет, {name}!"
* @param array<string, scalar> $params Динамические данные для подстановки
* @return string Готовая к отправке HTML-строка
*/
function formatTelegramHtmlTemplate(string $template, array $params): string
{
$replacements = [];
foreach ($params as $key => $value) {
$safeValue = htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$replacements['{' . $key . '}'] = $safeValue;
}
return strtr($template, $replacements);
}
Розбір помилок: ok:false та структура відповіді при збоях
При виникненні помилки Telegram Bot API завжди повертає JSON вигляду:
{
"ok": false,
"error_code": 400,
"description": "Bad Request: can't parse entities: Unexpected end tag at byte 45"
}Найбільш поширені коди помилок:
- 400 Bad Request: chat not found — Бот не може знайти чат. Виникає, якщо вказано неіснуючий
chat_idабо користувач не запускав бота. - 403 Forbidden: bot was blocked by the user — Користувач зупинив або заблокував бота. У CRM потрібно позначити цей
chat_idяк неактивний. - 429 Too Many Requests — Перевищено ліміт відправки повідомлень. У масиві
parametersприйде полеretry_afterіз вказівкою кількості секунд для паузи.
Інтеграція в сервісний шар застосунку
Для використання в реальних проєктах обгорніть логіку в сервісний клас, який бере токен із змінних оточення та ізолює бізнес-логіку від збоїв Telegram API.
<?php
class TelegramNotifier
{
private string $botToken;
public function __construct(?string $botToken = null)
{
$this->botToken = $botToken ?? (string)getenv('TELEGRAM_BOT_TOKEN');
if (empty($this->botToken)) {
throw new InvalidArgumentException("Telegram bot token is not configured in environment");
}
}
/**
* Отправка уведомления о новой заявке с сайта
*/
public function notifyNewLead(int|string $chatId, string $clientName, string $phone, string $comment): bool
{
$template = "<b>Новая заявка с сайта!</b>
"
. "<b>Имя:</b> {name}
"
. "<b>Телефон:</b> {phone}
"
. "<b>Комментарий:</b> {comment}";
$text = formatTelegramHtmlTemplate($template, [
'name' => $clientName,
'phone' => $phone,
'comment' => $comment,
]);
try {
sendTelegramApiMessage($this->botToken, $chatId, $text, 'HTML');
return true;
} catch (Throwable $e) {
// Запись ошибки в системный лог
error_log(sprintf("[TelegramNotifier Error] Chat: %s | Message: %s", $chatId, $e->getMessage()));
return false;
}
}
}
Такий підхід захистить основний застосунок: навіть якщо сервіси Telegram тимчасово недоступні або клієнт ввів спецсимволи в коментарі, форма на сайте відпрацює коректно, а факт помилки зафіксується в логах.
Розробляти збоєстійкі вебхуки та інтеграції бота з CRM зручно на базі платформи BotCreator.