Отправка сообщений через метод 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.