Отправка сообщений via the sendMessage method is the most frequent operation when working with the Telegram Bot API. However, in practice, developers often limit themselves to calling file_get_contents without checking the HTTP status code or validating JSON. In production, this approach leads to silent crashes, hanging scripts, and broken markup due to unescaped characters.
In this guide, we will look at how to build reliable message delivery in pure PHP using cURL, implement three-level error handling, and eliminate parsing failures with parse_mode=HTML.
Why file_get_contents Hurts Production
Using file_get_contents("https://api.telegram.org/bot...") is a classic anti-pattern in PHP for the following reasons:
- Lack of timeout control: By default, PHP will wait for a response until
default_socket_timeoutexpires (usually 60 seconds). If Telegram or a backbone provider experiences network congestion, your PHP-FPM worker processes will quickly block the entire socket pool. - Difficulty handling HTTP errors: If Telegram returns a
400 Bad Requestor429 Too Many Requestsstatus,file_get_contentswill trigger a PHP Warning and returnfalse, losing the response body where Telegram explains the error cause in detail (theparametersarray with theretry_afterfield). - Lack of flexible SSL configuration: You cannot explicitly configure connection and TLS handshake timeouts.
The solution is to use the cURL extension with explicit settings for CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT.
Three-Level Validation of Telegram API Response
A successful network request does not mean the message has been delivered. When processing Telegram's response, you need to go through three verification stages sequentially:
- Transport layer (cURL): Checking
curl_errno(). DNS errors, connection timeouts (error code 28), issues with SSL session resets. - HTTP protocol layer: Checking the response code via
CURLINFO_HTTP_CODE. Telegram returns200 OKon success, but sends400(invalid parameters),401(invalid token),403(bot blocked by user),409(webhook conflict), or429(rate limit exceeded) on errors. - Application layer (JSON Payload): Validating the response structure via
json_last_error()and checking the$response['ok'] === truefield.
Below is a basic transport module that implements this algorithm:
<?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'],
];
}
Escaping Special Characters with parse_mode=HTML
When sending formatted messages with the option 'parse_mode' => 'HTML', Telegram expects a valid HTML subset of tags (<b>, <i>, <a>, <code>, <pre>). If dynamic data (username, product title, comment text) contains characters like <, >, &, or ", Telegram will return a 400 Bad Request: can't parse entities error, and the message will not be sent.
To protect the text, you must pass ALL external dynamic variables through htmlspecialchars() with a specific set of flags:
<?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>";
}
Integration: Sending a Message with Handling 429 Rate Limits
Let's bring it all together: we'll create a scenario for sending a service notification that accounts for proper handling of parse_mode, response status checking, and analysis of the retry_after delay when exceeding Telegram API rate limits.
<?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}
";
}
Common Production Pitfalls
- Forgotten Content-Type: If you pass
json_encodewithout specifying theContent-Type: application/jsonheader, cURL will send the data asmultipart/form-dataorapplication/x-www-form-urlencoded, causing unpredictable behavior from the Telegram parser. - Missing the ENT_QUOTES option: Standard
htmlspecialchars($str)without flags does not escape single quotes, which with certain markup can break<a href="...">links. - Ignoring error_code 403: If a user has blocked the bot, the API will return
403 Forbidden: bot was blocked by the user. You should catch this status and mark the user as inactive in your database so as not to waste your request quota.
If you need assistance designing fault-tolerant bot architecture or building complex Telegram integrations, contact the specialists at BotCreator.