rest · REST API 26.600.0 Ломающее

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

1 мин чтения

Ломающее обновление

Удалены или изменены публичные API — прикладной код может перестать работать.

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

Проверьте, как вы создаёте команды встроек

У AddEmbeddingCommand и DeleteEmbeddingCommand из Bitrix\Rest\Public\Command\Application\Embedding переименовали параметры конструктора. Раньше currentUserId означал того, кто действует, а userId означал того, для кого встройка. Теперь действующий пользователь передаётся в userId, а целевой в targetUserId.

Порядок аргументов не менялся, поэтому позиционные вызовы работают как раньше. Именованный вызов со старым currentUserId: сразу падает с Error про неизвестный параметр. Смешанный вызов вида new AddEmbeddingCommand($app, $adminId, $placement, $handler, userId: $managerId) тоже упадёт, потому что userId уже передан вторым позиционным аргументом.

Новый вызов выглядит так:

        <?php declare(strict_types=1);

use Bitrix\Main\Loader;
use Bitrix\Rest\Internal\Repository\Application\AppRepository;
use Bitrix\Rest\Public\Command\Application\Embedding\AddEmbeddingCommand;

Loader::requireModule('rest');

$app = (new AppRepository())->getByClientId('local.6650a1b2c3d4e5.12345678');
if ($app === null)
{
    throw new \RuntimeException('Приложение не найдено');
}

$result = (new AddEmbeddingCommand(
    app: $app,
    userId: $adminId,              // кто ставит встройку
    placement: 'CRM_DEAL_DETAIL_TAB',
    handler: 'https://example.com/embed/deal-tab',
    targetUserId: $managerId,      // для кого; по умолчанию 0, как раньше
))->run();

if (!$result->isSuccess())
{
    // $result->getErrorMessages()
}

    

С внутренним EmbeddingUninstaller::uninstall() хуже, там ошибка может пройти молча. Его четвёртый параметр ?int $userId переименован в ?int $targetUserId, а следом добавлен int $userId = 0 для действующего пользователя. Если вы звали uninstall(..., userId: 5), имея в виду «удалить встройку пользователя 5», теперь пятёрка уйдёт в действующего пользователя, а USER_ID в запрос на удаление не попадёт вовсе. С userId: null получите TypeError.

Поменялось и поведение DeleteEmbeddingCommand при ошибке. Её execute() больше не заворачивает RestException в Result. Снаружи команду вызывают через run(), а AbstractCommand::run() из main оборачивает любое \Exception в CommandException. Раньше run() возвращал Result с ошибкой, теперь бросает CommandException, у которого в getPrevious() лежит исходная RestException. В REST-контроллерах модуля исключение разбирает CommandRunner. AccessException он превращает в AccessDeniedException, RestException с кодом ERROR_CORE во внутреннюю ошибку, остальные в ошибку валидации запроса.

Наследникам RestController: типы, фильтры и select по умолчанию

В Bitrix\Rest\V3\Controller\RestController свойство $responseLanguage стало ?string со значением null, а getResponseLanguage() теперь возвращает string и, если язык равен null, подставляет DefaultLanguage::get() (через ??=). Если наследник переобъявил свойство как string или метод с типом ?string, класс не загрузится.

RestController теперь сам переопределяет getDefaultPreFilters() и getDefaultPostFilters() и кладёт в обе цепочки один и тот же экземпляр IdempotencyFilter. Экземпляр общий, потому что в onBeforeAction фильтр запоминает состояние, которое потом читает onAfterAction. В docblock класса написано, что наследник, забывший parent::, «silently lose idempotency support». Если вы добавляете свои фильтры, делайте это поверх родительских:

        <?php declare(strict_types=1);

namespace Vendor\Shop\Rest\Controller;

use Bitrix\Rest\V3\Controller\RestController;
use Vendor\Shop\Rest\Filter\AuditFilter;

final class Order extends RestController
{
    protected function getDefaultPreFilters()
    {
        return [
            ...parent::getDefaultPreFilters(),
            new AuditFilter(),
        ];
    }
}

    

Ошибки привязки параметров (BinderArgumentException) раньше всегда превращались во внутреннюю ошибку. Теперь это RequiredFieldInRequestException, InvalidRequestFieldTypeException или RequestValidationException. Разбирают их по тексту сообщения исключения, через str_starts_with и регулярку. Если имя параметра определить не удалось или текст не подошёл ни под один шаблон, по-прежнему будет InternalException. Клиенты ваших методов вместо «внутренней ошибки» увидят ошибку валидации, и если они разбирают ответ по коду, это стоит проверить.

OrmRepository без select раньше отдавал в ORM ['*'], теперь собирает выборку из скалярных полей DTO. Если маппинг DTO опирался на колонки, которых в DTO нет, их в выборке не окажется.

Вложенные DTO без #[RelationToOne]/#[RelationToMany] теперь считаются встроенными. Их заполняет MappedBy, а в select работают пути вида address.city. ORM-связь внутри такого встроенного DTO в select даёт InvalidSelectException.

Приложения могут вызывать placement.* без прав администратора

Легаси-методы placement.bind, placement.unbind и placement.get раньше пускали только администратора. Теперь Bitrix\Rest\Api\Placement::checkPermission() пускает и обычного пользователя, если разрешает AppAccessChecker. Для bind нужно право ставить встройки, для unbind право удалять, для get право видеть список. Администратор определяется по user_id из авторизации запроса.

Не-админ может указать в USER_ID только себя или PlacementTable::DEFAULT_USER_ID_VALUE, иначе AccessException. placement.get научился принимать USER_ID, а без него не-админ видит только общие встройки и свои. placement.unbind без USER_ID у не-админа удаляет в тех же пределах. У администратора всё как было.

Одновременно права на встройки ужесточили. Для персонального приложения ставить и удалять встройки может только админ или владелец. Для остальных нужно прежнее право на локальные приложения и вдобавок условие «админ или приложение локальное». Для приложения, которое не персональное и не локальное (например, из маркетплейса), canInstallEmbedding() и canUninstallEmbedding() теперь требуют администратора. Список встроек в V3 без прав администратора показывает только общие и свои.

Если вы наследуете Bitrix\Rest\Api\Placement и переопределяли checkPermission(\CRestServer $server), объявление стало несовместимым: у родителя появился второй необязательный параметр.

Вебхуки без интеграции теперь можно обновить и удалить

UpdateIncomingWebhookCommandHandler и DeleteIncomingWebhookCommandHandler раньше бросали ObjectNotFoundException, если у вебхука нет записи интеграции. Теперь такой вебхук обновляется или удаляется напрямую через PasswordTable и PermissionTable в транзакции. После коммита кеш сбрасывается ещё раз. В комментарии объяснено, что параллельный запрос может успеть закешировать незакоммиченное состояние.

Результат Provider::saveIntegration() и Provider::deleteIntegration() раньше игнорировался, теперь при ошибке бросается PersistenceException с причинами в тексте. Scope системного вебхука менять нельзя, будет ArgumentException. Обновление и удаление пишутся в журнал безопасности и при неудаче, в записи есть поле outcome (success, failure, partialFailure) и тип вебхука.

Рядом похожая правка: UserRepository::save() при неудачном CUser::Update() бросает новый UserNotUpdatedException с текстом LAST_ERROR. Раньше результат обновления не проверялся.

Отложенный batch: пачка команд уходит в очередь

Новый V3-контроллер DeferredBatch принимает пачку команд и выполняет её в фоне. В коде встречаются имена методов rest.deferredbatch.add и rest.deferredbatch.downloadResult, а у контроллера пять действий: add, list, get, delete, downloadResult. Scope у всех глобальный, записи видны только их автору.

  1. add принимает fields.commands в формате обычного V3 batch и проверяет, что у вызывающего есть scope для каждой команды. Неизвестный метод даёт INVALID_METHOD, нехватка прав даёт INSUFFICIENT_SCOPE.
  2. Запись ложится в b_rest_deferred_batch со статусом pending, а в очередь Messenger rest.deferred_batch уходит сообщение с её ID.
  3. Обработчик очереди забирает запись атомарным UPDATE и выполняет batch. Если воркер умер, запись через 600 секунд можно забрать снова.
  4. Результат сохраняется gzip-JSON-файлом в b_file, статус становится done или error.
  5. downloadResult отдаёт подписанную ссылку на скачивание. Скачать может только автор и только в статусе done.

Очередь описана в .settings.php модуля: limit 5, total_processing_limit 20, три повтора с задержкой от 30 до 300 секунд. Агент Cleanup раз в сутки удаляет записи done и error старше 30 дней вместе с файлами.

С лимитом выборки похоже на баг. Docblock DeferredModeAwareInterface и DeferredRestApiServer обещают, что в отложенном режиме OrmRepository не применит лимит по умолчанию (PaginationStructure::DEFAULT_LIMIT, в docblock это 50 записей) и списочный метод вернёт всё. Код этого не делает. setDeferredMode(true) вызывается только из OrmActionTrait::getOrmRepositoryByRequest() и только если isOrmDeferredMode() вернул true. Но трейт сам объявляет isOrmDeferredMode() с return false, а метод трейта перекрывает унаследованный RestController::isOrmDeferredMode(), который как раз проверяет сервер. В контроллерах на OrmActionTrait лимит внутри отложенного batch остаётся. Если вы собирались выгрузить так весь список без пагинации, проверьте результат. По коду вы получите только первую страницу.

До обработки в колонке REQUEST_PARAMS лежат auth-параметры исходного запроса (array_merge($queryParams, $this->getServer()->getAuth())). Их обнуляют после выполнения, в коде так и написано: «so secrets do not linger in DB». Записи, которые застряли в pending, агент очистки не трогает. Если очередь на портале не разбирается, они там и останутся вместе с авторизацией. Проверить можно так:

        <?php declare(strict_types=1);

use Bitrix\Main\Loader;
use Bitrix\Main\Type\DateTime;
use Bitrix\Rest\Internal\Entity\DeferredBatch\Status;
use Bitrix\Rest\Internal\Model\DeferredBatchTable;

Loader::requireModule('rest');

$stuck = DeferredBatchTable::query()
    ->setSelect(['ID', 'USER_ID', 'CREATED_AT'])
    ->where('STATUS', Status::Pending->value)
    ->where('CREATED_AT', '<', (new DateTime())->add('-1 day'))
    ->fetchAll();

    

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

Idempotency-Key: повторный запрос не создаст вторую запись

Клиент V3 может прислать заголовок Idempotency-Key со строкой из печатных ASCII-символов без пробелов длиной до 255, например с UUID. Если запрос с тем же ключом и тем же телом придёт повторно, действие не выполнится. Сервер вернёт сохранённый ответ и заголовок Idempotent-Replayed: true. Тот же ключ с другим телом даст ошибку 422 Unprocessable Entity, кривое значение заголовка даст 400.

Подробности, которые влияют на клиента:

  • ключ хранится вместе с приложением, пользователем и методом, так что одинаковый ключ в разных методах не конфликтует;
  • тело сравнивается по sha256 сырого входа, поэтому повтор нужно отправлять теми же байтами: JSON, пересобранный с другим порядком ключей, получит 422;
  • сохраняются только ответы со статусом 200, ошибку при повторе выполнят заново;
  • запись живёт сутки (86400 с), если не задано иное.

Если клиент прислал заголовок, механизм включается сам для всех действий с параметром AddRequest, UpdateRequest или DeleteRequest, в том числе в ваших контроллерах. Управлять можно атрибутами, NotIdempotent сильнее. Ниже набросок, тела методов заменены заглушками:

        <?php declare(strict_types=1);

namespace Vendor\Shop\Rest\Controller;

use Bitrix\Rest\V3\Attribute\DtoType;
use Bitrix\Rest\V3\Attribute\Idempotent;
use Bitrix\Rest\V3\Attribute\NotIdempotent;
use Bitrix\Rest\V3\Controller\RestController;
use Bitrix\Rest\V3\Interaction\Request\AddRequest;
use Bitrix\Rest\V3\Interaction\Request\UpdateRequest;
use Bitrix\Rest\V3\Interaction\Response\AddResponse;
use Bitrix\Rest\V3\Interaction\Response\UpdateResponse;
use Vendor\Shop\Rest\Dto\OrderDto;

#[DtoType(OrderDto::class)]
final class Order extends RestController
{
    #[Idempotent(ttl: 3600)]
    public function addAction(AddRequest $request): AddResponse
    {
        // создаём заказ; повтор с тем же ключом в течение часа вернёт этот же ответ
        throw new \LogicException('Not implemented');
    }

    #[NotIdempotent]
    public function updateAction(UpdateRequest $request): UpdateResponse
    {
        // тут повторное применение безопасно, кеш ответа не нужен
        throw new \LogicException('Not implemented');
    }
}

    

Работает это только на V3-транспорте, AJAX-вызовы и легаси CRestServer фильтр пропускает. Ответы хранятся в PersistentStorageInterface из main. Если хранилище упало, ошибка уйдёт в лог, а действие выполнится без идемпотентности.

Разработчики признают в комментарии, что у хранилища нет атомарного setIfNotExists и два одновременных запроса с одним ключом выполнят действие оба («The second save() wins»). Повтор отсекается, только если первый запрос успел завершиться и сохранить ответ до прихода второго.

DynamicEnum: перечисление, значения которого живут в базе

Для DTO V3 появился атрибут #[DynamicEnum]. Он нужен, когда у поля конечный набор значений, но зашить его в BackedEnum нельзя, потому что набор хранится в базе. Провайдер реализует DynamicEnumProvider и возвращает строки или целые числа:

        <?php declare(strict_types=1);

namespace Vendor\Shop\Rest\Enum;

use Bitrix\Main\Loader;
use Bitrix\Rest\V3\Dto\DynamicEnum\DynamicEnumProvider;
use Bitrix\Rest\V3\Dto\DynamicEnum\DynamicEnumType;
use Bitrix\Sale\Internals\StatusTable;

final class OrderStatusEnum implements DynamicEnumProvider
{
    public function getType(): DynamicEnumType
    {
        return DynamicEnumType::String;
    }

    public function getValues(): array
    {
        Loader::requireModule('sale');

        return array_column(
            StatusTable::query()->setSelect(['ID'])->where('TYPE', 'O')->fetchAll(),
            'ID',
        );
    }
}

    
        <?php declare(strict_types=1);

namespace Vendor\Shop\Rest\Dto;

use Bitrix\Rest\V3\Attribute\DynamicEnum;
use Bitrix\Rest\V3\Dto\Dto;
use Vendor\Shop\Rest\Enum\OrderStatusEnum;

final class OrderDto extends Dto
{
    public ?int $id;

    #[DynamicEnum(OrderStatusEnum::class)]
    public string $statusId;
}

    

Значения проверяются в fields, в filter и в курсоре, а в OpenAPI поле описывается как enum. Ограничения такие:

  • PHP-тип свойства должен совпадать с типом перечисления (string для String, int для Int), совмещать атрибут с BackedEnum нельзя, иначе SystemException при разборе DTO;
  • провайдер создаётся через new $providerClass(), так что конструктор должен обходиться без аргументов;
  • значения кешируются в managed cache и в статическом массиве процесса.

Когда набор значений изменился, сбросьте кеш:

        \Bitrix\Rest\V3\Dto\DynamicEnum\DynamicEnumRegistry::clear(OrderStatusEnum::class);

    

clear() заодно увеличивает счётчик поколения, и закешированная OpenAPI-документация перегенерируется. Статический кеш он чистит только в текущем процессе, долгоживущий воркер будет помнить старые значения до перезапуска.

Таблица b_rest_deferred_batch

db.diff пуст, но в install/migrations/tables.php появилась таблица b_rest_deferred_batch: USER_ID, STATUS (по умолчанию pending), REQUEST_PARAMS, COMMANDS, SCOPES, RESULT_FILE_ID, ERROR_MESSAGE, CREATED_AT, UPDATED_AT. Индексы построены по (USER_ID, ID) и по (STATUS, CREATED_AT, ID). В install/migrations/agents.php добавлен агент очистки. Скрипта обновления в отчёте нет, поэтому по диффу не видно, как таблица и агент появятся на уже установленном портале. После обновления проверьте это сами.

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

  • В коде есть ссылки на внутренние проектные документы: «ALG-SCOPE-CHECK from SDD-backend-deferred-batch-2026-05-26.md», «spec design doc, Resolution 0.1», «design doc "Trade-off: accepted race window"».
  • Docblock IdempotencyKeyResolver описывает префикс rest.idempotency., а в константе rest.v3.idempotency.. Длина «prefix 20» при этом посчитана по настоящей константе.
  • Docblock DeferredRestApiServer обещает настроить пользователя и scope из сохранённых данных. В классе при этом только isDeferredMode() и пустой initRequestScope() плюс пять неиспользуемых импортов. Scope выставляет обработчик очереди через setAvailableScopes(), он же пересоздаёт global $USER как new \CUser().
  • DeferredBatch::deleteAction() сначала удаляет запись, потом файл. Если файл не удалился, клиент получит FILE_DELETE_FAILED, хотя записи уже нет.
  • Исправлен DeactivateSystemUserCommandHandler: он искал приложение через AppTable::getByClientId(), передавая туда числовой ID. Теперь getById(), а у ошибки появился код REST_APPLICATION_NOT_FOUND.
  • В гриде интеграций кнопку удаления показывали, если ID пользователя совпадал с ID интеграции. Теперь сравнивают с USER_ID владельца. Заодно появились подтверждение удаления и сортировка через ?by=&order= с белым списком полей.
  • Степпер Bitrix\Rest\Update\DeleteApplicationSystemUsers, который удалял или деактивировал системных пользователей приложений, выпотрошен, execute() сразу возвращает FINISH_EXECUTION.
  • Ветка в AppTable, которая срабатывает при включённой опции allow_create_sys_user, теперь пропускает локальные приложения. Тело условия в диффе не видно, по имени опции это создание системного пользователя.
  • Новая команда DeactivateIntegrationsCommand в одной транзакции гасит приложение и вебхуки системного пользователя. На любую ошибку, включая сбой БД, она отвечает текстом «Unknown system user integration type».
  • Вебхук из hold-списка перегрузки теперь отсекается до запроса в БД, а пароль не строкой сразу даёт INVALID_CREDENTIALS.
  • Если авторизация ответила «Https required.», V3 вернёт отдельную HttpsRequiredException (400) вместо «доступ запрещён».
  • В настройках модуля появилась политика rest_incoming_webhook_create_rights рядом с прежней rest_incoming_webhook_create_own_rights.
  • В мобильном меню пунктов маркета больше нет у экстранета и коллаберов.
  • ControllerData::fromArray() читал ключ dto, теперь dtoFqcn. В SchemaManager DTO восстанавливаются только для найденного модуля, в комментарии причина: getMethodDescriptions() «is reachable before auth».

Что делать

  • Найдите в своём коде new AddEmbeddingCommand(, new DeleteEmbeddingCommand( и вызовы EmbeddingUninstaller::uninstall() с именованными аргументами и переведите их на userId / targetUserId.
  • Проверьте наследников RestController: тип $responseLanguage и getResponseLanguage(), вызов parent:: в getDefaultPreFilters() и getDefaultPostFilters(), DTO с вложенными объектами без атрибутов связи.
  • Если у ваших V3-методов есть действия, которые нельзя повторять по сохранённому ответу, пометьте их #[NotIdempotent].
  • Если приложение вызывает placement.* от имени обычного пользователя, проверьте, что оно передаёт USER_ID только свой или общий.
  • После обновления проверьте, что таблица b_rest_deferred_batch и агент Cleanup на месте и что очередь rest.deferred_batch разбирается.

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

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 мин
voximplant 26.700.0 Ломающее Свежее

Voximplant 26.700.0: оценку качества связи убрали, у notifyAdmins() появился тип

Модуль телефонии voximplant 26.700.0 вышел в коробочный Битрикс24 11 сентября 2026 года, по данным официального канала «Битрикс24 changelog». Из карточки звонка убрали оценку качества связи вместе с её JS-событиями, у `Im::notifyAdmins()` появился тип параметра, а публичные константы `SipStatusInfor...

3 мин
ui 26.687.0 Рутинное Свежее

UI 26.687.0: триал VibePlus в облаке и дизайн чипа TintedBitrixGpt

`Bitrix\UI\Controller\InfoHelper` при активации демо сначала спрашивает у модуля bitrix24, включён ли старт VibePlus, и если да, запускает триал VibePlus вместо обычного демо тарифа. Ветка срабатывает только при подключённом модуле bitrix24 и определённой константе `BX24_HOST_NAME`. В `ui.system.chi...

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

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

на связи

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

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

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

Войти