Rest 26.650.0: тарифные ограничения Vibe+ и установка приложений через блокировку
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
rest 26.650.0 (сборка от 06.08.2026, 78 файлов, +3003/−371 строк) почти целиком про тарифные ограничения REST для модели монетизации Bitrix24 «Vibe+». Тарифные решения принимаются по данным модуля bitrix24, и на коробке без него ограничения не включаются. Часть изменений касается всех редакций: установка приложений теперь идёт через блокировку и откат, user.get перестал делать лишние запросы, а у нескольких классов модуля поменялись контракты. Deprecated нет, методы не удалялись.
Проверьте свои реализации и наследников классов rest
Internal\Contract\Repository\IncomingWebhookRepositoryInterface получил метод exists(WebhookFilter $filter): bool. Своя реализация интерфейса без него не загрузится. Если отдельного запроса «есть ли хоть одна запись» у вас нет, хватит такого:
<?php declare(strict_types=1);
namespace Vendor\Rest\Repository;
use Bitrix\Rest\Internal\Contract\Repository\IncomingWebhookRepositoryInterface;
use Bitrix\Rest\Internal\Repository\IncomingWebhook\WebhookFilter;
final class AuditedWebhookRepository implements IncomingWebhookRepositoryInterface
{
// ...остальные методы интерфейса
public function exists(WebhookFilter $filter): bool
{
return $this->getCount($filter) > 0;
}
}
У CreateIncomingWebhookCommandHandler появился собственный конструктор: репозиторий вебхуков, репозиторий интеграций и ?TariffAccessService. Раньше конструктор наследовался от AbstractCreateIncomingWebhookCommandHandler, и третьим параметром был SecurityAuditLogger. Код, который передавал логгер третьим аргументом, получит TypeError. Подменить логгер в этом обработчике теперь нельзя, в родителя уходят только два репозитория.
Остальные изменения сигнатур опасны для наследников. Классы не final, и переопределение со старой сигнатурой станет несовместимым:
Engine\Access::isFeatureEnabled()получил тип возврата: bool;Engine\Access::isAvailableCount()получил параметрbool $force = false;Marketplace\Client::isSubscriptionAccess()получил тип возврата: bool;Api\User::getUserData()получил параметр?array $userFieldMetadata = null;Preset\Provider::saveIntegration()получил параметрbool $skipTariffCheck = false;AbstractCreateIncomingWebhookCommandHandler::createIntegration()иApplicationInstaller::installAsPersonal()получили такой жеbool $skipTariffCheck = false;Internal\Repository\Application\AppRepository::getCount()получил параметрint $cacheTtl = 0;Infrastructure\Rest\Controller\Portal\License::getAction()получил третий обязательный параметрEntity\Portal\RestAvailability $restAvailability.
Установка приложений идёт через блокировку и откат
Новый ApplicationInstallationFinalizer теперь отвечает за момент, когда приложение становится установленным. Он переключает соединение на мастер (useMasterOnly(true)), берёт именованную блокировку БД rest_market_application_installation с таймаутом 5 секунд, заново считает лимит приложений (Access::isAvailableCount(..., force: true)) и только потом ставит INSTALLED и ACTIVE. Если лимит превышен, приложение деактивируется и удаляется.
Установка из маркета (Marketplace\Application и соответствующая ветка в CRestUtil) теперь сначала сохраняет ещё не установленное приложение как неактивное и неустановленное, потом вызывает финализатор. Если он отказал, модуль просит OAuth-сервис удалить приложение (unInstallApplication), ошибки этого вызова глушатся.
Через финализатор теперь идут и действие set_installed в компоненте app.layout, и импорт конфигурации. Раньше AppTable::install() и запись в журнал установки выполнялись, даже если обновление записи не удалось. Теперь только при успехе, а импорт добавляет в уведомления ошибку с кодом.
Коды ошибок финализатора:
APPLICATION_INSTALLATION_LOCK_NOT_ACQUIRED— блокировку не удалось взять за 5 секунд;MARKET_APPLICATION_LIMIT_EXCEEDED— исчерпан лимит приложений, в ответ установки добавляютсяhelperCodeи, если есть модульmarket,vibePlusApplicationLimit;APPLICATION_INSTALLATION_UPDATE_FAILED— не удалось обновить запись;APPLICATION_INSTALLATION_ROLLBACK_FAILED— не удался откат после отказа.
Если вы массово ставите приложения скриптом, параллельные установки теперь выстраиваются в очередь на блокировке, и те, что прождали дольше 5 секунд, получат APPLICATION_INSTALLATION_LOCK_NOT_ACQUIRED.
Обработайте код FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN
Отказ в авторизации вебхука или OAuth-приложения из-за ограничений раньше всегда приходил как ACCESS_DENIED с текстом «REST is available only by subscription.» или «...only on commercial plans.». Если теперь отказ вызван тарифом Vibe+, приходит error = FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN со статусом 403, остальные отказы остались ACCESS_DENIED. Если в bitrix24 есть сервис upsell-проекции, в ответ подмешиваются ещё и его поля. В V3 то же самое оформлено исключением FeatureNotAvailableOnCurrentPlanException со статусом 403.
Интеграциям, которые ходят в облачные порталы, стоит отличать этот код от обычного отказа в доступе:
<?php declare(strict_types=1);
namespace Vendor\Sync\Portal;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
final class WebhookClient
{
public function __construct(private readonly string $webhookUrl) {}
public function call(string $method, array $params = []): mixed
{
$http = new HttpClient(['socketTimeout' => 10, 'streamTimeout' => 30]);
$raw = $http->post(rtrim($this->webhookUrl, '/') . '/' . $method . '.json', $params);
$response = Json::decode((string)$raw);
return match ($response['error'] ?? null) {
null => $response['result'] ?? null,
'FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN' => throw new PlanRestrictedException(
(string)($response['error_description'] ?? ''),
),
default => throw new \RuntimeException(
$response['error'] . ': ' . ($response['error_description'] ?? ''),
),
};
}
}
PlanRestrictedException здесь ваш класс. Такую ошибку нет смысла ретраить, её надо показать администратору портала.
Внутри портала то же ограничение проявляется исключением. Preset\Provider::saveIntegration() в начале, до сохранения, зовёт TariffAccessService::ensurePresetAvailable() и может бросить Internal\Exception\VibePlus\FeatureNotAvailableOnCurrentPlanException. Раньше метод сообщал об ошибках только массивом с status и errors. Та же проверка стоит в InstallLocalAppCommandHandler, InstallPersonalAppCommandHandler, CreateIncomingWebhookCommandHandler и в компоненте rest.hook.ap.edit перед созданием вебхука. Если вы вызываете это из своего кода, ловите FeatureNotAvailableOnCurrentPlanExceptionInterface. У saveIntegration(), ApplicationInstaller::installAsPersonal() и createIntegration() появился параметр skipTariffCheck, и true в него передают только «принудительные» обработчики ForceInstallPersonalAppCommandHandler и ForceCreateIncomingWebhookCommandHandler.
Исходящие события могут не дойти
Когда тариф запрещает пользовательские вебхуки, Event\Sender молча пропускает часть обработчиков событий. Продолжают получать события опубликованные приложения (статус не STATUS_LOCAL) и системные обработчики, у которых нет ни APP_ID, ни INTEGRATION_ID, ни USER_ID. Пропускаются локальные приложения, пользовательские интеграции и обработчики с USER_ID. Отдельно отлавливаются старые боты: обработчик ONIMBOT*, чей токен через Im\Model\BotTable ведёт к интеграции с USER_ID > 0, считается пользовательским. Для этого Event\Callback теперь выбирает ещё INTEGRATION_ID и APP_STATUS.
Ошибки при этом нет, событие не отправляется. Если в облаке у клиента перестали приходить события в локальное приложение, начните с тарифа.
Как устроены ограничения Vibe+
Решения принимает новый Internal\Service\VibePlus\TariffAccessService, он доступен через ServiceContainer::getVibePlusTariffAccessService(). Он читает фичи тарифа rest_access и rest_access_transition_period и переменную market_application_limit, а состояние запуска Vibe+ (дата старта, переходный период, текущая редакция) берёт у RuntimeStateProvider из модуля bitrix24. Если модуля нет, зависимости сервиса пустые: getRestAccessAvailability() возвращает null, методы ensure*() ничего не бросают, лимит приложений не применяется. Если модуль есть, а провайдера состояния нет, в лог rest.vibe_plus.tariff_access один раз уходит warning «tariff restrictions are not applied».
Когда лимит маркет-приложений применим, считают по-новому. Раньше в выборку попадали все активные приложения, теперь установленные со статусами маркета (FREE, PAID, DEMO, TRIAL, SUBSCRIPTION). В счётчик раньше шли только бесплатные, теперь все нелокальные. Порог для ещё не установленного приложения прежний, оно блокируется при count >= limit. Поменялось поведение для уже установленного: раньше его тоже отсекали, если счётчик превышал лимит (count > limit), а при лимите Vibe+ эта проверка к нему не применяется.
Для своих экранов и отчётов есть публичные сервисы. VibePlusUsageProvider отдаёт «сырые» счётчики, а VibePlusMarketApplicationLimitProvider отдаёт проекцию лимита:
<?php declare(strict_types=1);
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Loader;
use Bitrix\Rest\Public\Enum\VibePlus\MarketApplicationLimitState;
use Bitrix\Rest\Public\Service\VibePlusMarketApplicationLimitProvider;
Loader::requireModule('rest');
$limit = ServiceLocator::getInstance()
->get(VibePlusMarketApplicationLimitProvider::class)
->getProjection();
if ($limit->getState() === MarketApplicationLimitState::Finite && $limit->isInstallationBlocked())
{
$message = sprintf('Установлено %d из %d, новое приложение из маркета не встанет', $limit->getCount(), $limit->getLimit());
}
Состояний четыре: finite, unlimited, notApplicable (модель монетизации не Vibe+ или дата запуска ещё не наступила) и unknown (не удалось получить лимит). На коробке без bitrix24 вы получите unknown.
Рядом поменялась логика подписки маркета. Marketplace\Client::isSubscriptionAccess() при установленном bitrix24 раньше проверял регион по префиксу лицензии в списке ru, ua, by с датами старта для двух последних. Теперь условие такое: модель монетизации SUBSCRIPTION или регион лицензии ru либо by. Украина из списка выпала, константы с регионами и датами удалены. В MarketSubscription проверка региона ru заменена проверкой модели монетизации, и без модуля bitrix24 эта проверка возвращает false.
user.get стал дешевле
Формат ответа прежний, а запросов к базе стало меньше:
checkAllowedFields()не вызывается, а метаданные пользовательских полей не грузятся при трёх условиях:selectзадан явно (не пустой и без*), в нём нетUF_-полей, кромеUF_DEPARTMENT, а в сортировке и фильтре нет ни одногоUF_-поля, включаяUF_DEPARTMENT;- метаданные UF запрашиваются один раз на весь вызов, а не для каждой строки, и без
LANGUAGE_ID: в комментарии сказано, что читаются толькоUSER_TYPE_IDиMULTIPLE; - фильтр по явному списку
ID, который помещается в первую страницу, обходится безCOUNT, а для одного ID ставитсяLIMIT 1; - список пользователей экстранет-групп запрашивается только в тех ветках, где он нужен.
БД
db.diff пуст. В install/migrations/tables.php в таблице входящих вебхуков b_rest_ap появилась колонка DATE_EXPIRE типа date, а ещё добавлена новая таблица b_rest_app_install_request: код и версия приложения, USER_ID, CHECK_HASH, INSTALL_HASH, название, иконка, STATUS (по умолчанию pending), RESOLVED_BY_ID, даты создания и решения. Кода, который с ними работает, в релизе нет: ни ORM-класса для новой таблицы, ни обращений к DATE_EXPIRE. Скрипта обновления в отчёте тоже нет.
Мелочи и находки
- Вебхуки и приложения с внешним атрибутом
vibecodeconnector=Yне попадают в счётчик пользовательских, отдельно считаются ключи сdeveloper_key=Y. Приложения с новым системным атрибутомFORCE_INSTALLEDтоже исключены из подсчёта пользовательских интеграций. LicenseScannerStateInvalidator::reset()дёргаетBitrix\Bitrix24\LicenseScanner\Manager::resetComputedState()после изменений приложений, вебхуков, интеграций, их атрибутов и списка бесплатных приложений. Вызов обложенclass_exists,method_existsиcatch (\Throwable)на случай старой версииbitrix24.- В
rest.integration.edit/class.phpдобавленuse ...\AppInstallCompletionMode, который нигде не используется, а файла с таким классом в релизе нет. - В
SystemIncomingWebhookCreatorисправлена утечка состояния:EventController::enableEvents()переехал вfinally. Раньше исключение при создании системного вебхука оставляло события пресетов выключенными до конца запроса. Preset\Provider::saveIntegration()теперь проставляетINTEGRATION_IDобработчикам событий интеграции, и новым, и уже существующим.- Ответ контроллера лицензии портала (
Portal\License::getAction()) получил ключrestс полемisAvailable. - Подсказки об ограничениях в формах интеграций берут код из upsell-проекции
bitrix24, а зашитыйlimit_subscription_market_access_buy_marketplusостался запасным вариантом. Marketplace\Immune::load()теперь обновляет и статический список иммунных приложений, а не только кеш.
Что делать
- Если вы реализуете
IncomingWebhookRepositoryInterface, добавьтеexists(). - Если создаёте
CreateIncomingWebhookCommandHandlerруками с тремя аргументами, уберите третий или передайте тудаTariffAccessService. - Если вызываете
Provider::saveIntegration(), команды установки приложений или создания вебхука из своего кода, ловитеFeatureNotAvailableOnCurrentPlanExceptionInterface. - В интеграциях с облачными порталами обработайте
FEATURE_NOT_AVAILABLE_ON_CURRENT_PLANотдельно отACCESS_DENIED. - Если ставите приложения скриптом или импортом конфигурации, проверьте обработку новых кодов ошибок установки.