Разработчики часто сталкиваются с ситуацией, когда отправка уведомлений через 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.