rest · REST API 26.900.0 Заметное

Rest 26.900.0: встройки без приложения, чтение приложений без AppTable и заготовка MCP

2 мин чтения
Узнавайте о ломающих изменениях Битрикса раньше, чем они доедут до прода: свежие разборы — в канале @bxmax_updates

В rest 26.900.0 у встроек появился владелец, и модуль может повесить встройку на свою сущность без REST-приложения. Приложение читается в типизированный DTO через ApplicationProvider::find*(), статус оплаты считает ApplicationStatusCalculator, а для REST 3.0 заложен мост в MCP, который в БУС не работает. Сломается прямой вызов CRestUtil::CleanApp() с CLIENT_ID и запись в PlacementTable с неполным владельцем или APP_ID = 0.

Новое

Читайте приложение через ApplicationProvider

У ApplicationProvider появились findById(), findByClientId() и findByCode(). Все три возвращают ?ApplicationData, readonly-DTO с типизированными геттерами вроде isInstalled(): ?bool, getStatus(): ?ApplicationStatus и getDateFinish(): ?Date. findByCode() дороже двух других, индекса по CODE в b_rest_app нет. В отличие от getByClientId($userId, $clientId) того же провайдера, новые методы не проверяют ни права пользователя, ни INSTALLED, это на вас.

Статус оплаты считает ApplicationStatusCalculator::calculate(), результат лежит в ApplicationStatusData: isPaymentAllowed(), isPaymentExpired(), isPaymentNotifyNeeded(), getDaysLeft(), isHeld(). Правила перенесены из getAppStatusInfo(): предупреждение за 5 дней, 14 льготных дней для платного приложения, потом откат в демо, если есть демо-URL и триал не использован. Время, подписку Маркета и hold калькулятор получает через интерфейсы из Bitrix\Rest\Public\Contract\Application\Status, так что в тесте «сегодня» можно зафиксировать.

        <?php declare(strict_types=1);

namespace Vendor\Billing\Rest;

use Bitrix\Rest\Public\Provider\ApplicationProvider;
use Bitrix\Rest\Public\Service\ApplicationStatusCalculator;

final class AppPaymentGuard
{
    public function __construct(
        private readonly ApplicationProvider $apps,
        private readonly ApplicationStatusCalculator $calculator,
    ) {}

    public function canCall(string $clientId): bool
    {
        $app = $this->apps->findByClientId($clientId);
        if ($app === null || $app->isActive() !== true || $app->isInstalled() !== true) {
            return false;
        }

        return $this->calculator->calculate($app)->isPaymentAllowed();
    }
}

    

Повесьте встройку на свою сущность

Владелец встройки задаётся тремя полями: MODULE_ID, ENTITY_TYPE и ENTITY_ID. У встроек приложений это rest/app/<APP_ID>, и заполняется тройка сама. Другой модуль объявляет провайдера в своём .settings.php: 'rest.embedding.ownerProviders' => ['value' => ['article' => ArticleEmbeddingOwner::class]]. Провайдер реализует EmbeddingOwnerProviderInterface: getEmbeddingAccessCodes() отдаёт коды доступа новой встройки (AU, U1, G5 и другие коды CAccess), а resolveEmbeddings() превращает встройки в EmbeddingOpenDto с URL и payload для открытия.

        <?php declare(strict_types=1);

namespace Vendor\Kb\Integration\Rest;

use Bitrix\Rest\Public\Dto\Embedding\EmbeddingOwnerDto;
use Bitrix\Rest\Public\Provider\Dto\Embedding\EmbeddingDto;
use Bitrix\Rest\Public\Provider\Dto\Embedding\EmbeddingOpenDto;
use Bitrix\Rest\Public\Provider\EmbeddingOwnerProviderInterface;

final class ArticleEmbeddingOwner implements EmbeddingOwnerProviderInterface
{
    public function getEmbeddingAccessCodes(EmbeddingOwnerDto $owner): array
    {
        return ['AU'];
    }

    public function resolveEmbeddings(array $embeddings, int $userId): array
    {
        return array_map(
            static fn(EmbeddingDto $embedding): EmbeddingOpenDto => new EmbeddingOpenDto(
                embeddingId: $embedding->id,
                url: (string)$embedding->handler,
                payload: ['ARTICLE_ID' => (int)$embedding->entityId],
            ),
            $embeddings,
        );
    }
}

    

Привязывает встройку BindEmbeddingCommand, отвязывает UnbindEmbeddingCommand по ID встройки:

        <?php declare(strict_types=1);

use Bitrix\Main\Loader;
use Bitrix\Rest\Public\Command\Embedding\BindEmbeddingCommand;
use Bitrix\Rest\Public\Dto\Embedding\EmbeddingLanguageDto;
use Bitrix\Rest\Public\Dto\Embedding\EmbeddingOwnerDto;

Loader::requireModule('rest');

$result = (new BindEmbeddingCommand(
    owner: new EmbeddingOwnerDto('vendor.kb', 'article', '42'),
    placement: 'VENDOR_KB_PANEL', // объявлен вашим модулем в OnRestServiceBuildDescription
    handler: 'https://kb.example.ru/embed/42',
    languages: [new EmbeddingLanguageDto('ru', title: 'Статья из базы знаний')],
))->run();

$embeddingId = $result->isSuccess() ? $result->getData()['embeddingId'] : null;

    

Команда требует существующий неприватный плейсмент и абсолютный http(s)-URL обработчика, учитывает max_count, user_mode и registerCallback плейсмента и пишет всё в транзакции. Ожидаемые отказы (EMBEDDING_ALREADY_EXISTS, EMBEDDING_BINDING_NOT_SUPPORTED, ошибки плейсмента) приходят в Result, а ошибки аргументов и ненайденный провайдер владельца вылетают из run() как CommandException.

К REST-приложению так ничего не привязать, потому что AppEmbeddingOwnerProvider::getEmbeddingAccessCodes() бросает EmbeddingBindingNotSupportedException. Других модулей с провайдерами в эталоне нет.

Стандартный интерфейс выводит встройки через старый PlacementTable::getHandlersList(), который берёт только строки с активным приложением. Свои встройки вы покажете сами через EmbeddingProvider::getListByPlacement($placement, $userId): он вернёт EmbeddingViewDto с локализованным заголовком, иконкой, URL и payload от провайдера. Администратор видит все общие и свои встройки, остальные только открытые их кодам доступа. При пустом списке кодов встройку увидят только администраторы.

REST 3.0 → MCP: пока заготовка

Атрибут #[McpTool] помечает метод контроллера REST 3.0 для экспорта в MCP-инструмент, набор инструментов собирает наследник RestToolSetProvider. Он наследует класс из модуля aiassistant, которого в БУС нет, а #[McpTool] в эталоне не стоит ни на одном контроллере, так что в БУС мост не заработает.

Скоупы вебхуков сравниваются без учёта регистра

PermissionTable::cleanPermissionList() вычищает скоупы, которые вебхуку выдавать нельзя: rating, entity, placement, landing_cloud и глобальный. Раньше сравнение было регистрозависимым, и Placement сохранялся в правах вебхука. В списке прав он оставался, но методы placement.* не открывал, потому что проверка доступа тоже регистрозависима. Теперь скоупы сравниваются в нижнем регистре и без пробелов по краям, а дубли вида crm/CRM у вебхуков и приложений из «Интеграций» схлопываются.

AppTable::delete() удаляет привязки приложения

AppTable::delete() теперь сам удаляет обработчики событий, офлайн-обработчики, встройки и коды доступа приложения. Раньше привязки чистились только у локальных приложений и только при зарегистрированном OAuth. Сбой одного шага пишется в лог и не мешает остальным.

Что сломается

CleanApp с CLIENT_ID бросает исключение

PlacementTable::deleteByApp() и PlacementLangTable::deleteByApp() при ID ≤ 0 бросают Main\ArgumentException. Раньше строка приводилась к 0, и удалялись встройки с APP_ID = 0, обычно ни одной. CRestUtil::CleanApp($appId, $bClean) передаёт ID дальше как есть, поэтому вызов с CLIENT_ID или кодом приложения теперь падает, причём обработчики OnRestAppDelete к этому моменту уже отработали. Передавайте числовой ID, например (new ApplicationProvider())->findByClientId($clientId)?->getId(), и проверьте результат на null.

AppTable::uninstall() сам переводит CLIENT_ID в ID, поэтому через него падения нет, и OnRestAppDelete при таком вызове получает числовой APP_ID.

onAfterPlacementDelete приходит на каждую встройку

Старый deleteByApp() выбирал встройки с 'limit' => 1, и событие onAfterPlacementDelete::<PLACEMENT> уходило только для первой. Теперь оно приходит на каждую удалённую встройку. Обработчик, который ждал одного вызова на удаление приложения, получит несколько. Сделайте его идемпотентным.

PlacementTable проверяет владельца при записи

PlacementTable::add() и update() валидируют тройку владельца. Обычный add() с положительным APP_ID работает как раньше, тройка заполнится значениями rest/app/<APP_ID>. Ошибку вы получите в трёх случаях:

  • передали только часть тройки: MODULE_ID, ENTITY_TYPE and ENTITY_ID must be changed together;
  • значение длиннее 50 символов;
  • явно передали APP_ID ≤ 0 без тройки или APP_ID, который не совпадает с ENTITY_ID владельца rest/app: Invalid embedding owner.

Сломается код, который создавал встройки без приложения через APP_ID = 0.

Deprecated

Новых @deprecated нет, но сам модуль начал уходить от двух старых методов. В десяти файлах вместо AppTable::getByClientId() он вызывает ApplicationProvider::findByClientId() и findById(). ApplicationStatusCalculator::calculate() заменил AppTable::getAppStatusInfo() в рассылке событий Event\Callback и Event\Sender, а события REST 3.0 и компоненты Маркета по-прежнему зовут getAppStatusInfo(). Калькулятор отдаёт статус, флаги и число дней: подстановок для текста уведомлений (MESSAGE_REPLACE с #DETAIL_URL#, #DAYS#, #CODE#) в нём нет, а 'Y'/'N' стали bool.

БД

В b_rest_placement добавлены MODULE_ID varchar(50) (по умолчанию rest), ENTITY_TYPE varchar(50) (по умолчанию app) и ENTITY_ID varchar(50), плюс уникальный индекс по владельцу, плейсменту и обработчику. У старых строк ENTITY_ID пустой, владельца модуль вычисляет по APP_ID. Встройки приложения ищите по APP_ID, а не по ENTITY_ID. У встроек других владельцев APP_ID равен NULL.

Новая таблица b_rest_embedding_access (EMBEDDING_ID, ACCESS_CODE varchar(100)) хранит коды доступа встроек. Для встроек приложения коды берутся из его поля ACCESS. У обычного приложения пустой ACCESS даёт AU. У личного к кодам из ACCESS добавляется U<владелец>, а AU при пустом ACCESS не ставится. Существующие встройки заполняет степпер BackfillEmbeddingAccess, его регистрации в эталоне не видно.

Мелочи и находки

  • ApplicationData::toArray() и __debugInfo() включают ClientSecret, SharedKey и ApplicationToken. var_dump() или DTO в логе целиком раскроет секреты приложения, логируйте отдельные поля.
  • Конструкторы InstallPersonalAppCommandHandler и CreateIncomingWebhookCommandHandler потеряли TariffAccessService: тарифная проверка дублировалась и осталась в пресете. Заденет только тех, кто собирает эти хендлеры руками. У AppTable::clearClientCache() и PlacementTable::checkUniq() поменялись protected-сигнатуры, проверьте наследников, если они их переопределяют.

Новая версия вышла — разбор уже в Telegram

В канале @bxmax_updates публикуем каждый свежий разбор: что сломается, что устарело и что появилось в API. Узнаете до того, как обновите прод.

Подписаться

Читайте дальше

rest 26.800.0 Ломающее Свежее

Rest 26.700.0–26.800.0: отложенная установка приложений, строгий bool в REST 3.0 и вебхуки со сроком действия

В БУС вышли подряд две версии `rest`: 26.700.0 и 26.800.0. В 26.700.0 у входящих вебхуков появился срок действия, а сотрудник без права установки может попросить администратора поставить приложение. В 26.800.0 поменялось время срабатывания `OnRestAppInstall`, REST 3.0 стал строже проверять входные д…

2 мин
rest 26.600.0 Ломающее Свежее

Rest 26.600.0: отложенный batch и Idempotency-Key в REST V3, новые правила для встроек

В `rest` 26.600.0 (сборка от 23.07.2026) 143 файла и +5143/−383 строк. В REST V3 появились отложенный batch со своей таблицей и очередью, идемпотентность по заголовку `Idempotency-Key` и перечисления, значения которых берутся из кода. Заодно переделали права на встройки и сигнатуры команд встроек. К…

1 мин
rest 26.650.0 Ломающее Свежее

Rest 26.650.0: тарифные ограничения Vibe+ и установка приложений через блокировку

`rest` 26.650.0 (сборка от 06.08.2026, 78 файлов, +3003/−371 строк) почти целиком про тарифные ограничения REST для модели монетизации Bitrix24 «Vibe+». Тарифные решения принимаются по данным модуля `bitrix24`, и на коробке без него ограничения не включаются. Часть изменений касается всех редакций:…

1 мин
rest 26.500.0 Безопасность

rest 26.500.0: журнал аудита безопасности и курсорные ответы в V3

REST завёл единый журнал того, что происходит с приложениями, правами и вебхуками, а установщик переехал с SQL-батчей на миграции ядра. Публичные REST-методы не тронуты — ломается внутреннее и V3-API, так что читать этот разбор стоит тем, кто пишет свои модули поверх `rest`, а не тем, кто дёргает `c…

3 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…

Можно закрыть чат и перейти на другую страницу. Когда ответ будет готов, на кнопке Домового появится отметка.

Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти