Надежная отправка sendMessage в Telegram на PHP: cURL, обработка ошибок API и экранирование HTML

Многие разработчики начинают интеграцию с Telegram Bot API самым простым путем: вызывают file_get_contents("https://api.telegram.org/bot$token/sendMessage?..."). Это классическая ошибка, которая в продакшене быстро приводит к зависанию PHP-FPM воркеров, падению производительности сервера и уязвимостям к инъекциям. Любой сетевой сбой на стороне Telegram или задержка ответа заблокируют ваш поток выполнения.

В этой статье мы разберем, как построить отказоустойчивую отправку сообщений через sendMessage с использованием cURL, настроить жесткие таймауты, корректно обработать все типы ошибок (включая сетевые сбои, некорректный JSON и статус ok: false) и безопасно экранировать пользовательские данные при использовании parse_mode: HTML.

Почему file_get_contents не подходит для Telegram API

Функция file_get_contents отправляет GET-запрос без гибкой настройки заголовков и таймаутов. Если шлюз Telegram будет отвечать дольше обычного (например, под высокой нагрузкой), ваш PHP-скрипт зависнет до истечения дефолтного таймаута PHP (обычно 60 секунд). При плотном потоке пользователей это мгновенно исчерпает свободные воркеры веб-сервера.

Кроме того, отправка конфиденциальных данных (таких как персональные данные клиентов или токены) через GET-параметры в строке URL логируется прокси-серверами и веб-серверами по пути следования трафика. Правильный подход — отправка строго POST-запросов с телом в формате JSON через cURL.

Шаг 1. Базовый cURL-запрос с таймаутами

Для надежного сетевого взаимодействия нам понадобятся два ключевых параметра cURL: CURLOPT_CONNECTTIMEOUT (максимальное время ожидания установки соединения) и CURLOPT_TIMEOUT (максимальное время на выполнение всего запроса). Для Telegram Bot API оптимально выставлять 3-5 секунд на коннект и не более 10 секунд на получение ответа.

Ниже представлен базовый пример функции для работы с API Telegram через POST JSON:

<?php

function sendTelegramRequest(string $method, array $payload): array
{
$token = getenv("TELEGRAM_BOT_TOKEN");
if (!$token) {
throw new RuntimeException("Telegram bot token is not configured in environment variables.");
}

$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),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 4, // 4 секунды на подключение
CURLOPT_TIMEOUT => 8, // 8 секунд лимит на весь запрос
CURLOPT_HTTPHEADER => [
'Content-Type: application/json'
],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);

curl_close($ch);

if ($curlErrno !== 0) {
throw new RuntimeException("cURL Connection Error ({$curlErrno}): {$curlError}");
}

return [
'http_code' => $httpCode,
'body' => $response
];
}

Шаг 2. Обработка ошибок: HTTP-коды, json_last_error и ok:false

Успешный HTTP-статус от сервера еще не означает, что сообщение доставлено. Telegram возвращает статус 200 OK при успешной отправке, но при ошибках валидации (например, неверный chat_id или сломанная HTML-разметка) он вернет HTTP-код 400 Bad Request с JSON-телом, содержащим подробности.

Алгоритм разбора ответа должен состоять из трех этапов:

  1. Проверка системных ошибок cURL (сеть, DNS, таймауты).
  2. Валидация синтаксиса JSON с помощью json_last_error().
  3. Проверка внутреннего флага ok в ответе Telegram. Если ok === false, извлекаем description и error_code.

Вот как выглядит профессиональная обработка ответов:

<?php

function parseTelegramResponse(array $rawResult): array
{
$body = $rawResult['body'];
$httpCode = $rawResult['http_code'];

$data = json_decode($body, true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(
"Failed to parse Telegram JSON response. " .
"JSON Error: " . json_last_error_msg() . ". Raw response: " . substr($body, 0, 250)
);
}

if (!isset($data['ok']) || $data['ok'] !== true) {
$description = $data['description'] ?? 'Unknown error';
$errorCode = $data['error_code'] ?? $httpCode;

// Здесь вы можете обработать специфичные ошибки, например 429 (Too Many Requests)
throw new RuntimeException(
"Telegram API Error [Status {$errorCode}]: {$description}"
);
}

return $data['result'];
}

Шаг 3. Безопасный parse_mode HTML и экранирование

При отправке сообщений с форматированием (жирный текст, курсив, ссылки) разработчики часто выбирают parse_mode => 'HTML'. Это удобно, но таит в себе скрытую угрозу. Если в тексте сообщения окажется спецсимвол (например, угловая скобка <, > или амперсанд &), Telegram не сможет распарсить разметку и вернет ошибку 400 Bad Request: can't parse entities, а пользователь не получит уведомление.

Чтобы этого избежать, любой динамический контент (имена пользователей, тексты отзывов, значения из БД) необходимо экранировать с помощью htmlspecialchars() со строгими флагами. При этом статические теги оформления (такие как <b>, <i>, <code>) должны оставаться нетронутыми.

<?php

function safeHtml(string $text): string
{
// Экранируем символы <, >, &, " и ' в соответствии с требованиями Telegram HTML
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}

// Пример формирования безопасного сообщения
$userName = "<Ivan> & Co";
$userComment = "Хочу заказать разработку <strong>срочно</strong>!";

$messageText = "<b>Новая заявка!</b>\n\n" .
"Клиент: " . safeHtml($userName) . "\n" .
"Комментарий: <code>" . safeHtml($userComment) . "</code>";

// Результат безопасен для отправки, парсер Telegram не упадет.

Шаг 4. Объединяем все в класс-клиент для продакшена

Соберем все лучшие практики в один лаконичный класс, который можно легко внедрить в любой PHP-проект, будь то чистый PHP, Laravel или контроллер Yii2.

<?php

class TelegramNotifier
{
private string $token;

public function __construct()
{
$this->token = (string) getenv('TELEGRAM_BOT_TOKEN');
if (empty($this->token)) {
throw new InvalidArgumentException("Telegram Bot Token environment variable is missing.");
}
}

public function sendMessage(int $chatId, string $htmlText): bool
{
$payload = [
'chat_id' => $chatId,
'text' => $htmlText,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true
];

try {
$url = "https://api.telegram.org/bot" . $this->token . "/sendMessage";
$ch = curl_init();

curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 7,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
curl_close($ch);

if ($curlErrno !== 0) {
error_log("Telegram cURL Network Error ({$curlErrno}): {$curlError}");
return false;
}

$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Telegram JSON Decode Error: " . json_last_error_msg() . ". Response: " . $response);
return false;
}

if (!isset($data['ok']) || !$data['ok']) {
$desc = $data['description'] ?? 'No description';
$code = $data['error_code'] ?? $httpCode;
error_log("Telegram API Error [{$code}]: {$desc} (Chat ID: {$chatId})");
return false;
}

return true;

} catch (Throwable $e) {
error_log("Fatal error in TelegramNotifier: " . $e->getMessage());
return false;
}
}
}

Заключение

Для надежной отправки сообщений в Telegram Bot API всегда используйте cURL с POST JSON-телом вместо небезопасного file_get_contents. Обязательно ограничивайте таймаут соединения (connect timeout) и общее время выполнения, чтобы не забивать ресурсы сервера. Применение htmlspecialchars для динамических данных убережет ваших ботов от падений из-за некорректного синтаксиса разметки HTML.

Если вам требуется разработка отказоустойчивых Telegram-решений, интеграция с CRM-системами и автоматизация бизнес-процессов, доверьте это профессионалам из BotCreator.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.