Telegram Webhook Controller in Yii2: CSRF Bypass, Secret Token Validation, and Yii Queue

When developing Telegram-бот s on the Yii2 framework, developers often encounter architectural problems. Telegram requires the server to respond to webhook requests almost instantly (within 1-2 seconds). If your code starts performing heavy operations — sending messages, querying external CRMs, or complex database calculations — Telegram breaks the connection due to timeout and begins sending retries. This leads to a snowball effect of increased load and message duplication for users.

In this article we will build a professional, fault-tolerant Webhook receiver on Yii2. We will solve the token validation problem, bypass built-in CSRF protection, implement strict idempotency by saving update_id to the database, and move all business logic to a background queue using the yii2-queue extension.

Step 1: Database and ensuring idempotency

Idempotency guarantees that the same update from Telegram will not be processed twice. Network failures are common: Telegram may send a request before waiting for a response due to a second-of-a-second network lag, consider delivery failed, and send the same update_id again.

To protect against duplicates, we will create a simple table in the database where the unique update_id serves as the primary key. We use the standard Yii2 migration mechanism.

use yii\db\Migration;

class m231024_120000_create_tg_updates_table extends Migration
{
public function safeUp()
{
$this->createTable("{{%tg_processed_update}}", [
"update_id" => $this->bigInteger()->notNull()->unsigned(),
"processed_at" => $this->timestamp()->defaultExpression("CURRENT_TIMESTAMP"),
]);
$this->addPrimaryKey("pk-tg_processed_update-update_id", "{{%tg_processed_update}}", "update_id");
}

public function safeDown()
{
$this->dropTable("{{%tg_processed_update}}");
}
}

A repeated insert of an already processed update_id will trigger a uniqueness constraint violation error (Integrity Constraint Violation), which we can easily catch in the controller and return a successful status 200 OK to Telegram, preventing reprocessing.

Step 2: Creating a Webhook controller with CSRF disabledBy default, Yii2 protects all POST requests using CSRF tokens. Since requests from Telegram come from outside, Yii2 blocks them with a 400 Bad Request error. We need to disable CSRF validation specifically for the webhook action.

Also, we will add a check for the secret header X-Telegram-Bot-Api-Secret-Token, which we specify when registering the webhook through the setWebhook method. This ensures that requests to our URL really come from Telegram servers, not attackers who have learned the script address.

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use yii\web\Response;
use app\queue\TelegramProcessorJob;

class TelegramController extends Controller
{
// Отключаем CSRF-валидацию для работы внешнего вебхука
public $enableCsrfValidation = false;

public function actionWebhook()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$request = Yii::$app->request;

// Защита: проверяем <a href="/blog/telegram-webhook-pure-php-setup">секретный токен</a>, заданный при setWebhook
$expectedToken = Yii::$app->params["telegram_secret_token"] ?? null;
$receivedToken = $request->headers->get("X-Telegram-Bot-Api-Secret-Token");

if ($expectedToken && $receivedToken !== $expectedToken) {
Yii::warning("Попытка несанкционированного доступа к вебхуку", "telegram");
throw new BadRequestHttpException("Invalid secret token");
}

$rawBody = $request->getRawBody();
$update = json_decode($rawBody, true);

if (json_last_error() !== JSON_ERROR_NONE || !isset($update["update_id"])) {
return ["status" => "error", "message" => "Invalid JSON or missing update_id"];
}

$updateId = $update["update_id"];

// Проверяем идемпотентность через попытку вставки в базу данных
try {
Yii::$app->db->createCommand()
->insert("{{%tg_processed_update}}", ["update_id" => $updateId])
->execute();
} catch (\yii\db\Exception $e) {
// Если запись уже существует (код ошибки 23000 / 1062 дубликат)
if ($e->errorInfo[1] == 1062 || strpos($e->getMessage(), "23000") !== false) {
return ["status" => "ok", "message" => "Duplicate update ignored"];
}
throw $e;
}

// Отправляем задачу в очередь Yii Queue для асинхронной обработки
Yii::$app->queue->push(new TelegramProcessorJob([
"update" => $update,
]));

// Моментально отвечаем Telegram успехом
return ["status" => "ok"];
}
}

Step 3: Asynchronous event processing via Yii Queue

For background processing, we use the official component yiisoft/yii2-queue. It allows tasks to be queued in Redis, RabbitMQ, DB, or Gearman and executed by console workers of the daemon.

We will create a Job class that will handle the direct processing of incoming messages and sending responses to the user.

namespace app\queue;

use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;

class TelegramProcessorJob extends BaseObject implements JobInterface
{
/** @var array Входящий массив данных от Telegram */
public $update;

public function execute($queue)
{
if (!isset($this->update["message"]["chat"]["id"])) {
return;
}

$chatId = $this->update["message"]["chat"]["id"];
$text = $this->update["message"]["text"] ?? "";

// Бизнес-логика бота
if (strpos($text, "/start") === 0) {
$this->sendMessage($chatId, "Привет! Ваша команда принята и обработана асинхронно через Yii Queue.");
}
}

private function sendMessage($chatId, $text)
{
$token = Yii::$app->params["telegram_bot_token"] ?? null;
if (!$token) {
Yii::error("Токен <a href="/blog/php-telegram-bot-getupdates-long-polling-local-dev">Telegram Bot API</a> не сконфигурирован", "telegram");
return;
}

$url = "https://api.telegram.org/bot{$token}/sendMessage";
$payload = json_encode([
"chat_id" => $chatId,
"text" => $text,
"parse_mode" => "HTML"
]);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);

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

if ($curlError) {
Yii::error("Ошибка cURL при отправке в Telegram: {$curlError}", "telegram");
return;
}

if ($httpCode !== 200) {
Yii::error("Telegram API вернул код {$httpCode}. Ответ: {$response}", "telegram");
return;
}

$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !($result["ok"] ?? false)) {
Yii::error("Некорректный ответ Telegram API: " . ($result["description"] ?? "Unknown"), "telegram");
}
}
}

Step 4: Application configuration

For proper system operation, add token parameters to the configuration file config/params.php:

return [
"telegram_bot_token" => getenv("TELEGRAM_BOT_TOKEN"),
"telegram_secret_token" => getenv("TELEGRAM_SECRET_TOKEN"), // Любая случайная строка
];

Also make sure the queue component is registered in both the console and web application configurations (config/web.php and config/console.php):

"components" => [
"queue" => [
"class" => \yii\queue\db\Queue::class,
"db" => "db", // Компонент подключения к БД
"tableName" => "{{%queue}}", // Таблица очереди
"channel" => "telegram",
"mutex" => \yii\mutex\MysqlMutex::class,
],
],

Typical errors when integrating webhooks in Yii2

  • Using file_get_contents(\"php://input\"): In Yii2, to get the raw request body, you should always use the method Yii::\\'->request->getRawBody(). It caches the result within the framework, excluding issues with re-reading the input stream.
  • No timeouts on cURL: Always explicitly set CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT. Without them, a hanging request to the Telegram API will block the queue worker, reducing overall system throughput.
  • Sending response inside the HTTP session of the controller: Never execute long logic inside actionWebhook. Telegram will break the connection, and the user will receive a duplicated message since the server returns a timeout error, and Telegram will retry delivery.

The implemented architecture scales easily: under increasing load, it is sufficient to launch multiple queue workers using Supervisor with the command php yii queue/listen.

If you need professional development of complex integrations and bots on a contract basis, contact specialists BotCreator.

"}

New articles on Telegram

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