Отправка сообщений через метод sendMessage — самая частая операция при работе с Telegram Bot API. При этом на практике разработчики часто ограничиваются вызовом file_get_contents без проверки HTTP-кода и валидации JSON. В продакшене такой подход приводит к молчаливым падениям, «зависшим» скриптам и поломке разметки из-за незаэкранированных символов.
В этом руководстве мы разберем, как построить надежную отправку сообщений на чистом PHP с использованием cURL, реализовать трехуровневую обработку ошибок и исключить сбои парсинга parse_mode=HTML.
Почему file_get_contents вредит продакшену
Использование file_get_contents("https://api.telegram.org/bot...") — классическая антипрактика в PHP по следующим причинам:
- Отсутствие контроля таймаутов: По умолчанию PHP будет ждать ответа до истечения
default_socket_timeout(обычно 60 секунд). Если у Telegram или магистрального провайдера возникнет сетевой затор, ваши рабочие процессы PHP-FPM быстро заблокируют весь пул сокетов. - Сложность работы с HTTP-ошибками: Если Telegram вернет статус
400 Bad Requestили429 Too Many Requests,file_get_contentsвыбросит PHP Warning и вернетfalse, потеряв тело ответа, в котором Telegram подробнее объясняет причину ошибки (массивparametersс полемretry_after). - Отсутствие гибкой настройки SSL: Вы не можете явно настроить тайм-ауты соединения и рукопожатия TLS.
Решение — использование расширения cURL с явной установкой CURLOPT_CONNECTTIMEOUT и CURLOPT_TIMEOUT.
Трехуровневая валидация ответа Telegram API
Успешный сетевой запрос не означает, что сообщение доставлено. При обработке ответа Telegram нужно последовательно пройти три этапа проверки:
- Транспортный уровень (cURL): Проверка
curl_errno(). Ошибки DNS, таймаут соединения (error code 28), проблемы со сбросом SSL-сессии. - Уровень HTTP-протокола: Проверка кода ответа через
CURLINFO_HTTP_CODE. Telegram возвращает200 OKпри успехе, но при ошибках шлет400(неверные параметры),401(неверный токен),403(бот заблокирован пользователем),409(конфликт вебхуков) или429(превышение лимитов). - Уровень приложения (JSON Payload): Валидация структуры ответа через
json_last_error()и проверку поля$response['ok'] === true.
Ниже представлен базовый транспортный модуль, выполняющий этот алгоритм:
<?php
declare(strict_types=1);
function executeTelegramApi(string $method, array $payload): array
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN is not set in environment');
}
$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$rawResponse = curl_exec($ch);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 1. Проверка cURL ошибки
if ($curlErrno !== 0) {
return [
'ok' => false,
'error_type' => 'transport',
'description' => "cURL error ({$curlErrno}): {$curlError}",
];
}
// 2. Декодирование JSON и проверка json_last_error
$responseData = json_decode((string)$rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'ok' => false,
'error_type' => 'json',
'description' => 'JSON decode error: ' . json_last_error_msg(),
'raw_body' => $rawResponse,
];
}
// 3. Проверка флага ok от Telegram API и HTTP статуса
if ($httpCode !== 200 || empty($responseData['ok'])) {
return [
'ok' => false,
'error_type' => 'api',
'http_code' => $httpCode,
'error_code' => $responseData['error_code'] ?? $httpCode,
'description' => $responseData['description'] ?? 'Unknown Telegram Error',
'parameters' => $responseData['parameters'] ?? [],
];
}
return [
'ok' => true,
'result' => $responseData['result'],
];
}
Экранирование спецсимволов при parse_mode=HTML
При отправке форматированных сообщений с опцией 'parse_mode' => 'HTML' Telegram ожидает валидный HTML-подмножество тегов (<b>, <i>, <a>, <code>, <pre>). Если динамические данные (имя пользователя, название товара, текст комментария) содержат символы <, >, & или ", Telegram вернет ошибку 400 Bad Request: can't parse entities и сообщение не отправится.
Для защиты текста необходимо пропускать ВСЕ внешние динамические переменные через htmlspecialchars() со специальным набором флагов:
<?php
declare(strict_types=1);
/**
* Безопасное экранирование текста для Telegram HTML parse_mode
*/
function telegramEscape(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
/**
* Пример формирования сообщения с динамическими данными
*/
function buildOrderMessage(int $orderId, string $clientName, string $userComment): string
{
$safeName = telegramEscape($clientName);
$safeComment = telegramEscape($userComment);
return "<b>Новый заказ №{$orderId}</b>
" .
"Клиент: <i>{$safeName}</i>
" .
"Комментарий: <code>{$safeComment}</code>";
}
Интеграция: отправка сообщения с обработкой лимитов 429
Сберем всё вместе: создадим сценарий отправки сервисного уведомления, учитывающий корректную работу с parse_mode, проверкой статуса ответа и анализом задержки retry_after при превышении лимитов Telegram API.
<?php
declare(strict_types=1);
$chatId = (int)getenv('TELEGRAM_NOTIFY_CHAT_ID');
$rawUserInput = 'ООО "Вектор & Ко" <info@vector.test>'; // Данные с опасными символами
$messageText = buildOrderMessage(10854, $rawUserInput, 'Просьба перезвонить до 18:00');
$response = executeTelegramApi('sendMessage', [
'chat_id' => $chatId,
'text' => $messageText,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
]);
if (!$response['ok']) {
// Обработка ошибок
if ($response['error_type'] === 'api') {
// Ошибка 429: Too Many Requests
if ($response['http_code'] === 429 && isset($response['parameters']['retry_after'])) {
$waitSeconds = (int)$response['parameters']['retry_after'];
error_log("Telegram Rate Limit Hit. Need to wait {$waitSeconds} seconds.");
// Здесь можно отправить задачу повторно в очередь (RabbitMQ / Redis / Yii Queue)
} else {
error_log(sprintf(
"Telegram API Error [%d]: %s",
$response['error_code'],
$response['description']
));
}
} else {
error_log("Network/JSON Error: " . $response['description']);
}
} else {
$sentMessageId = $response['result']['message_id'];
echo "Сообщение успешно отправлено! ID: {$sentMessageId}
";
}
Частые ошибки в продакшене
- Забытый Content-Type: Если при передаче
json_encodeне указать заголовокContent-Type: application/json, cURL отправит данные какmultipart/form-dataилиapplication/x-www-form-urlencoded, что вызовет непредсказуемое поведение парсера Telegram. - Отсутствие опции ENT_QUOTES: Обычный
htmlspecialchars($str)без флагов не экранирует одиночные кавычки, что при определенной разметке может ломать ссылки<a href="...">. - Игнорирование error_code 403: Если пользователь заблокировал бота, API вернет
403 Forbidden: bot was blocked by the user. Вы должны перехватывать этот статус и помечать пользователя в своей БД как неактивного, чтобы не тратить квоту запросов.
Если вам нужна помощь в проектировании отказоустойчивой архитектуры ботов или выстраивании сложных Telegram-интеграций, обратитесь к специалистам BotCreator.