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 Requestor429 Too Many Requestsresponses, the function returnsfalseand 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-urlencodedorapplication/jsonformat requires cumbersome context configuration viastream_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:
- Network level (cURL): Checking for the absence of socket errors, timeouts, and SSL certificate issues (checking
curl_errno). - Syntax level (JSON): Validating the response structure using
json_decodeand checkingjson_last_error(). - Application level (Telegram Bot API): Analyzing the
okfield in the received array. Telegram may return HTTP 200 or 400, but ifok === 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 <b>. 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_idis 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_idas inactive. - 429 Too Many Requests — Message sending rate limit exceeded. The
parametersarray will contain aretry_afterfield 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.