Когда в вашем веб-приложении оформляется заказ или пользователь запрашивает уведомление через Telegram-бота, вашему бэкенду необходимо отправить исходящее сообщение с использованием Telegram Bot API. Разработчики часто прибегают к быстрым реализациям вроде  file_get_contents  или базового скрипта  curl_exec . На первый взгляд, если токен правильный и целевой ID чата валиден, сообщение доставляется мгновенно. Однако в продакшн-окружении такая наивная реализация приводит к незаметным сбоям, из-за которых заказы клиентов остаются неподтвержденными, а каналы оповещений молчат без генерации исключений на бэкенде.

Исходящий HTTP-запрос к Telegram может завершиться ошибкой на четырех различных уровнях: таймауты сетевых сокетов, HTTP-ответы, отличные от 200, некорректное тело JSON и флаги отклонения на уровне Telegram API ( ok: false ). Если ваш PHP-скрипт проверяет только то, что  curl_exec  возвращает значение, отличное от false, вы полностью упустите случаи, когда Telegram отклоняет ваш пейлоад из-за поврежденного HTML-форматирования, когда пользователь заблокировал бота или когда были превышены лимиты запросов (rate limits).

Путь к сбою: почему базовые вызовы cURL теряют сообщения

Рассмотрим стандартный скрипт вебхука интернет-магазина, обрабатывающий новую покупку. Он форматирует HTML-сводку заказа с введенными покупателем данными и отправляет POST-запрос на  sendMessage .

 <?php
// Bad Example: Do not use in production
$token = getenv('BOT_TOKEN');
$chatId = $_POST['user_id'];
$userName = $_POST['user_name']; // e.g., "AT&T / Customer <John>"

$message = "<b>New Order!</b>\nCustomer: " . $userName;

$ch = curl_init("https://api.telegram.org/bot{$token}/sendMessage");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => [
        'chat_id' => $chatId,
        'text' => $message,
        'parse_mode' => 'HTML'
    ]
]);

$result = curl_exec($ch);
curl_close($ch);

// Flawed check: $result is a JSON string, which evaluates to boolean true
if ($result) {
    echo "Message sent successfully!";
}
 

Когда этот скрипт выполняется в нормальных условиях с чистыми именами клиентов, он, судя по всему, работает. Но как только пользователь вводит специальные символы вроде  AT&T  или указывает неэкранированное имя вроде  Customer <John> , весь запрос падает. Поскольку  parse_mode  установлен в  HTML , Telegram ожидает теги в стиле валидного XML. При обнаружении неэкранированных амперсандов или незакрытых угловых скобок Telegram возвращает статус HTTP 400 с ошибкой  {"ok":false,"error_code":400,"description":"Bad Request: can't parse entities"} .

Поскольку наивный скрипт проверяет только  if ($result) , он интерпретирует строку ответа Telegram с HTTP 400 как успешное выполнение. Переменная  $result  содержит JSON-строку, возвращенную Telegram, которая при приведении к булеву типу в PHP оценивается как  true . В результате ваша логика обработки заказов считает, что уведомление было отправлено, не логирует ошибок и продолжает работу, оставляя пользователя без подтверждающего сообщения.

Более того, если эндпоинты Telegram API испытывают временную задержку (латентность) или потерю пакетов, cURL без явных таймаутов сокета будет зависать бесконечно. Если ваш пул PHP-FPM имеет 20 воркеров, 20 зависших cURL-запросов быстро исчерпают пул процессов вашего сервера, обрушив бэкенд-приложение для всех пользователей.

Санитизация динамического ввода для режима Telegram HTML

Telegram поддерживает ограниченный набор HTML-тегов:  <b> ,  <i> ,  <a> ,  <code> ,  <pre>  и  <u> . Любые символы за пределами этих поддерживаемых тегов — в частности  < ,  >  и  &  — должны быть преобразуены в соответствующие HTML-сущности. Использование  strip_tags()  опасно, поскольку он удаляет легитимный текст, похожий на HTML-теги, например, примечания клиентов, содержащие математически оформленный текст или пользовательские коды.

Чтобы безопасно внедрить пользовательский контент в сообщение с HTML-форматированием, вы должны пропустить каждую динамическую переменную через  htmlspecialchars()  перед ее интерполяцией в шаблон сообщения. Не передавайте всю готовую строку сообщения в  htmlspecialchars() , так как это экранирует ваши задуманные структурные теги вроде  <b>  в текст  &lt;b&gt; .

 <?php
declare(strict_types=1);

function formatOrderNotification(string $userName, string $productName, float $amount): string
{
    // Escape dynamic parameters individually before embedding
    $safeName = htmlspecialchars($userName, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    $safeProduct = htmlspecialchars($productName, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
    $formattedAmount = number_format($amount, 2);

    return "<b>New Order Received</b>\n\n" .
           "<b>Customer:</b> {$safeName}\n" .
           "<b>Item:</b> {$safeProduct}\n" .
           "<b>Total:</b> \${$formattedAmount}";
}
 

Обратите внимание на флаги, передаваемые в  htmlspecialchars :  ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 .  ENT_QUOTES  гарантирует экранирование как одинарных, так и двойных кавычек,  ENT_SUBSTITUTE  заменяет недопустимые последовательности кодовых единиц символом замены Unicode вместо возврата пустой строки, а  ENT_HTML5  гарантирует соответствие преобразования сущностей современным стандартам.

Создание четырехэтапного верификационного cURL-диспетчера

Чтобы гарантировать, что исходящее сообщение было получено и принято инфраструктурой Telegram, ваша PHP-обертка должна выполнить четыре последовательных шага валидации:

  1.  Уровень транспорта cURL: Проверьте  curl_errno($ch) , чтобы убедиться, что HTTP-запрос достиг серверов Telegram без сетевых таймаутов, сбоев поиска DNS или обрывов рукопожатия SSL.
  2.  Уровень HTTP-статус-кодов: Проверьте  curl_getinfo($ch, CURLINFO_HTTP_CODE) . Telegram возвращает HTTP 200 при успехе, HTTP 400 при некорректном пейлоаде, HTTP 403, когда бот заблокирован пользователем, и HTTP 429 при превышении лимитов.
  3.  Уровень парсинга JSON: Выполните  json_decode($response, true)  и немедленно проверьте  json_last_error() === JSON_ERROR_NONE . Некорректные ответы или страницы ошибок вышестоящего прокси, возвращающие чистый HTML, сломают последующие проверки массива, если их не провалидировать.
  4.  Уровень Telegram API: Проверьте  $data['ok'] === true . Если  ok  равен  false , извлеките  $data['description']  и  $data['error_code']  для запуска доменно-специфичной логики фолбэка.
 <?php
declare(strict_types=1);

class TelegramResponse
{
    public function __construct(
        public readonly bool $success,
        public readonly int $httpCode,
        public readonly ?array $data = null,
        public readonly ?string $errorMessage = null
    ) {}
}

function sendTelegramMessage(string $botToken, int|string $chatId, string $htmlText): TelegramResponse
{
    $url = "https://api.telegram.org/bot{$botToken}/sendMessage";

    $payload = json_encode([
        'chat_id' => $chatId,
        'text' => $htmlText,
        'parse_mode' => 'HTML',
        'disable_web_page_preview' => true
    ]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_CONNECTTIMEOUT => 5, // Timeout for connection setup
        CURLOPT_TIMEOUT => 10,        // Timeout for total execution
    ]);

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

    // Step 1: Check cURL Transport Error
    if ($curlErrno !== 0) {
        return new TelegramResponse(false, 0, null, "cURL Network Error ({$curlErrno}): {$curlError}");
    }

    // Step 2: Decode JSON
    /** @var array|null $decoded */
    $decoded = json_decode((string) $rawResponse, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        return new TelegramResponse(
            false, 
            $httpCode, 
            null, 
            "JSON Parse Error: " . json_last_error_msg() . " | Raw: " . substr((string)$rawResponse, 0, 100)
        );
    }

    // Step 3 & 4: Check HTTP Code and Telegram API Flag
    if ($httpCode !== 200 || !isset($decoded['ok']) || $decoded['ok'] !== true) {
        $apiDescription = $decoded['description'] ?? 'Unknown API error';
        $errorCode = $decoded['error_code'] ?? $httpCode;
        return new TelegramResponse(
            false, 
            $httpCode, 
            $decoded, 
            "Telegram API Error [{$errorCode}]: {$apiDescription}"
        );
    }

    return new TelegramResponse(true, $httpCode, $decoded);
}
 

Отправка тел в формате JSON через  CURLOPT_POSTFIELDS  с явными заголовками  Content-Type: application/json  предпочтительнее отправки сырых POST-массивов. Массивы вызывают кодирование  multipart/form-data  внутри cURL, что увеличивает размер полезной нагрузки и порождает потенциальные баги преобразования массива в строку при передаче булевых флагов или вложенных массивов.

Продакшн-обработка: повторные попытки, заблокированные пользователи и очищенные логи

В продакшн-системах сетевые вызовы неизбежно падают из-за временных проблем или лимитов. Ваша архитектура диспетчеризации должна корректно обрабатывать специфические статус-коды Telegram, не обрушая воркеры очереди и не утекая учетные данные окружения.

Лимиты запросов (HTTP 429)

Telegram накладывает ограничения на вызовы API (обычно 30 сообщений в секунду глобально по всем чатам или 1 сообщение в секунду в конкретном чате). Когда вы превышаете эти лимиты, Telegram возвращает HTTP 429 вместе с объектом  parameters , содержащим  retry_after .

Бот заблокирован или пользователь деактивирован (HTTP 403)

Когда пользователь блокирует вашего бота или удаляет свой аккаунт в Telegram, Telegram возвращает HTTP 403 с описанием  Forbidden: bot was blocked by the user . Не продолжайте попытки отправить сообщение этому пользователю при каждой запланированной фоновой задаче. Перехватите этот статус-код и обновите базу данных, пометив статус уведомлений пользователя как отключенный.

Очистка файлов логов

Токены Bot API, внедренные непосредственно в целевые URL (например,  https://api.telegram.org/bot<TOKEN>/sendMessage ), представляют собой угрозу безопасности, если записываются в логи в исходном виде на системных файлах или внешних платформах мониторинга. При логировании упавших cURL-вызовов или транспортных ошибок всегда маскируйте токен бота перед сохранением на диск.

 <?php
declare(strict_types=1);

function processOutboundMessage(string $token, int $userId, string $messageHtml, PDO $db): void
{
    $response = sendTelegramMessage($token, $userId, $messageHtml);

    if ($response->success) {
        // Message delivered successfully
        return;
    }

    $logContext = [
        'user_id' => $userId,
        'http_code' => $response->httpCode,
        'error' => $response->errorMessage,
    ];

    // Specific Error Handling
    if ($response->httpCode === 403) {
        // User blocked the bot: disable future notifications in DB
        $stmt = $db->prepare("UPDATE users SET telegram_notifications_enabled = 0 WHERE telegram_id = :id");
        $stmt->execute([':id' => $userId]);
        error_log("Telegram notification disabled for user {$userId}: User blocked bot.");
        return;
    }

    if ($response->httpCode === 429) {
        // Rate limited: extract retry_after parameter
        $retryAfter = $response->data['parameters']['retry_after'] ?? 5;
        error_log("Telegram Rate Limit Hit. Retry allowed after {$retryAfter} seconds.");
        // Here, re-queue the job with a delay matching $retryAfter
        return;
    }

    // Fallback: log error while sanitizing sensitive tokens
    $safeMessage = str_replace($token, '[REDACTED_TOKEN]', $response->errorMessage ?? '');
    error_log("Failed to send Telegram message to {$userId}: " . $safeMessage);
}
 

Надежная обработка исходящего взаимодействия с Telegram на PHP требует обработки каждого слоя жизненного цикла HTTP с помощью явных проверок, а не предположений об успешной передаче. Проверка транспортных ошибок cURL, верификация кодов ответа HTTP, безопасный парсинг JSON и экранирование пользовательского ввода гарантируют, что ваши фоновые воркеры будут работать чисто, не теряя критически важные обновления.

При создании сложных сценариев автоматизации или корпоративных интеграций инженерные команды полагаются на BotCreator — студию, которая создает Telegram-ботов и мини-приложения с архитектурой высокой доступности и надежными бэкенд-интеграциями.