Fault-Tolerant sendMessage in Telegram using PHP: cURL, API Error Handling, and Safe HTML

Developers often encounter situations where sending notifications via Telegram Bot API starts failing or silently losing messages. A common reason is a simplified implementation using file_get_contents without HTTP code checks, or unescaped text in parse_mode: HTML mode, which leads to the 400 Bad Request: can't parse entities error.

In a production environment, отправка сообщений must be a reliable process. This requires a full-fledged cURL client, three-level validation of server responses, and strict escaping of dynamic data.

Why file_get_contents Is Not Suitable for Production

Using file_get_contents('https://api.telegram.org/bot...') creates three key issues:

  • Lack of flexible timeouts: The function uses the system's default_socket_timeout. If Telegram or the gateway hangs, the script will be blocked for 60 seconds.
  • Difficulty collecting HTTP statuses: For 400 Bad Request or 429 Too Many Requests responses, the function returns false and generates a PHP Warning instead of returning the response body with error details.
  • Lack of precise header control: Sending data in application/x-www-form-urlencoded or application/json format requires cumbersome context configuration via stream_context_create.

Using cURL allows you to explicitly set network limits, retrieve the exact HTTP response code, and read the JSON containing the error from Telegram.

Three-Level System for Checking Telegram API Responses

Any request to api.telegram.org should go through three sequential validation stages:

  1. Network level (cURL): Checking for the absence of socket errors, timeouts, and SSL certificate issues (checking curl_errno).
  2. Syntax level (JSON): Validating the response structure using json_decode and checking json_last_error().
  3. Application level (Telegram Bot API): Analyzing the ok field in the received array. Telegram may return HTTP 200 or 400, but if ok === false, the request is considered unhandled.

Below is a basic client function implementing all three levels of validation:

<?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'];
}

Timeouts: CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT

When sending notifications, it is important to distinguish between two types of timeouts:

  • CURLOPT_CONNECTTIMEOUT: The waiting time for establishing an initial TCP/TLS connection with Telegram servers. A value of 3–5 seconds is more than enough.
  • CURLOPT_TIMEOUT: The total request execution time. For synchronous sending within a website's HTTP request, set it to 5–10 seconds so the script doesn't hang during issues on trunk networks.

If you need to send hundreds of messages, do not execute them sequentially inside a single user HTTP request. Store notifications in a database or queue (Yii Queue, Laravel Queues) and process them with a background daemon.

Safe parse_mode HTML and Special Character Escaping

The parse_mode: HTML mode allows formatting text using a limited set of tags: <b>, <i>, <code>, <pre>, <a href="...">. However, any unescaped <, >, or & character in user data will break the HTML tree, and Telegram will return a 400 Bad Request error.

To protect templates, you must not apply htmlspecialchars to the entire final message text—this would convert your <b> tags into &lt;b&gt;. You should escape exclusively dynamic insertions (names, comments, form data).

<?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);
}

Error Analysis: ok:false and Response Structure on Failures

{
"ok": false,
"error_code": 400,
"description": "Bad Request: can't parse entities: Unexpected end tag at byte 45"
}

The most common error codes:

  • 400 Bad Request: chat not found — The bot cannot find the chat. Occurs if a non-existent chat_id is specified or if the user has not started the bot.
  • 403 Forbidden: bot was blocked by the user — The user has stopped or blocked the bot. In the CRM, you need to mark this chat_id as inactive.
  • 429 Too Many Requests — Message sending rate limit exceeded. The parameters array will contain a retry_after field specifying the pause duration in seconds.

Integrating into the Application Service Layer

To use in real-world projects, wrap the logic in a service class that retrieves the token from environment variables and isolates business logic from Telegram API failures.

<?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;
}
}
}

This approach will protect the main application: even if Telegram services are temporarily unavailable or a client entered special characters in a comment, форма на сайте will execute correctly, and the error will be recorded in the logs.

Developing fault-tolerant webhooks and bot integrations with CRMs is convenient using the BotCreator platform.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.