Reliable sendMessage in Telegram via PHP: cURL, API Error Handling, and HTML Escaping

Many developers start integrating with Telegram Bot API the easiest way: by calling file_get_contents("https://api.telegram.org/bot$token/sendMessage?..."). This is a classic mistake that quickly leads to PHP-FPM worker hangs, server performance drops, and injection vulnerabilities in production. Any network failure on the Telegram side or response delay will block your execution thread.

In this article, we will look at how to build fault-tolerant message sending via sendMessage using cURL, configure strict timeouts, correctly handle all types of errors (including network failures, invalid JSON, and ok: false status), and safely escape user data when using parse_mode: HTML.

Why file_get_contents is not suitable for the Telegram API

The file_get_contents function sends a GET request without flexible configuration of headers and timeouts. If the Telegram gateway takes longer than usual to respond (for example, under high load), your PHP script will hang until the default PHP timeout expires (usually 60 seconds). With a heavy flow of users, this will instantly exhaust the web server's free workers.

In addition, sending sensitive data (such as customer personal data or tokens) via GET parameters in the URL string is logged by proxy servers and web servers along the traffic path. The correct approach is to send strictly POST requests with a JSON body via cURL.

Step 1. Basic cURL request with timeouts

For reliable network interaction, we will need two key cURL parameters: CURLOPT_CONNECTTIMEOUT (the maximum time to wait for a connection to be established) and CURLOPT_TIMEOUT (the maximum time to execute the entire request). For the Telegram Bot API, it is optimal to set 3-5 seconds for the connection and no more than 10 seconds to receive a response.

Below is a basic example of a function for working with the Telegram API via POST JSON:

<?php

function sendTelegramRequest(string $method, array $payload): array
{
$token = getenv("TELEGRAM_BOT_TOKEN");
if (!$token) {
throw new RuntimeException("Telegram bot token is not configured in environment variables.");
}

$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),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 4, // 4 секунды на подключение
CURLOPT_TIMEOUT => 8, // 8 секунд лимит на весь запрос
CURLOPT_HTTPHEADER => [
'Content-Type: application/json'
],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);

curl_close($ch);

if ($curlErrno !== 0) {
throw new RuntimeException("cURL Connection Error ({$curlErrno}): {$curlError}");
}

return [
'http_code' => $httpCode,
'body' => $response
];
}

Step 2. Error handling: HTTP codes, json_last_error, and ok:false

A successful HTTP status from the server does not yet mean that the message has been delivered. Telegram returns a 200 OK status on successful delivery, but in case of validation errors (for example, an invalid chat_id or broken HTML markup), it will return an HTTP code 400 Bad Request with a JSON body containing details.

The response parsing algorithm should consist of three stages:

  1. Checking for system cURL errors (network, DNS, timeouts).
  2. Validating JSON syntax using json_last_error().
  3. Checking the internal ok flag in the Telegram response. If ok === false, we extract description and error_code.

Here is what professional response handling looks like:

<?php

function parseTelegramResponse(array $rawResult): array
{
$body = $rawResult['body'];
$httpCode = $rawResult['http_code'];

$data = json_decode($body, true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(
"Failed to parse Telegram JSON response. " .
"JSON Error: " . json_last_error_msg() . ". Raw response: " . substr($body, 0, 250)
);
}

if (!isset($data['ok']) || $data['ok'] !== true) {
$description = $data['description'] ?? 'Unknown error';
$errorCode = $data['error_code'] ?? $httpCode;

// Здесь вы можете обработать специфичные ошибки, например 429 (Too Many Requests)
throw new RuntimeException(
"Telegram API Error [Status {$errorCode}]: {$description}"
);
}

return $data['result'];
}

Step 3. Safe HTML parse_mode and escaping

When sending formatted messages (bold text, italics, links), developers often choose parse_mode => 'HTML'. This is convenient, but it harbors a hidden threat. If a special character (for example, an angle bracket <, >, or an ampersand &) appears in the message text, Telegram will not be able to parse the markup and will return a 400 Bad Request: can't parse entities error, and the user will not receive the notification.

To avoid this, any dynamic content (usernames, review texts, database values) must be escaped using htmlspecialchars() with strict flags. At the same time, static formatting tags (such as <b>, <i>, <code>) must remain untouched.

<?php

function safeHtml(string $text): string
{
// Экранируем символы <, >, &, " и ' в соответствии с требованиями Telegram HTML
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}

// Пример формирования безопасного сообщения
$userName = "<Ivan> & Co";
$userComment = "Хочу заказать разработку <strong>срочно</strong>!";

$messageText = "<b>Новая заявка!</b>\n\n" .
"Клиент: " . safeHtml($userName) . "\n" .
"Комментарий: <code>" . safeHtml($userComment) . "</code>";

// Результат безопасен для отправки, парсер Telegram не упадет.

Step 4. Combining everything into a client class for production

Let's gather all the best practices into one concise class that can be easily integrated into any PHP project, whether it's pure PHP, Laravel, or контроллер Yii2.

<?php

class TelegramNotifier
{
private string $token;

public function __construct()
{
$this->token = (string) getenv('TELEGRAM_BOT_TOKEN');
if (empty($this->token)) {
throw new InvalidArgumentException("Telegram Bot Token environment variable is missing.");
}
}

public function sendMessage(int $chatId, string $htmlText): bool
{
$payload = [
'chat_id' => $chatId,
'text' => $htmlText,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true
];

try {
$url = "https://api.telegram.org/bot" . $this->token . "/sendMessage";
$ch = curl_init();

curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 7,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);

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

if ($curlErrno !== 0) {
error_log("Telegram cURL Network Error ({$curlErrno}): {$curlError}");
return false;
}

$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Telegram JSON Decode Error: " . json_last_error_msg() . ". Response: " . $response);
return false;
}

if (!isset($data['ok']) || !$data['ok']) {
$desc = $data['description'] ?? 'No description';
$code = $data['error_code'] ?? $httpCode;
error_log("Telegram API Error [{$code}]: {$desc} (Chat ID: {$chatId})");
return false;
}

return true;

} catch (Throwable $e) {
error_log("Fatal error in TelegramNotifier: " . $e->getMessage());
return false;
}
}
}

Conclusion

For reliable message sending to the Telegram Bot API, always use cURL with a POST JSON body instead of the insecure file_get_contents. Be sure to limit the connection timeout (connect timeout) and the total execution time so as not to clog server resources. Using htmlspecialchars for dynamic data will save your bots from crashing due to incorrect HTML markup syntax.

If you need the development of fault-tolerant Telegram solutions, integration with CRM systems and автоматизация бизнес-процессов, entrust this to the professionals from BotCreator.

New articles on Telegram

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