Rest 26.1000.0–26.1100.0: JSON-ответы потоком, свои права на методы полей REST 3.0 и встройки модулей в интерфейсе
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
В rest 26.1000.0 JSON-ответы REST пошли потоком, а модуль теперь сам решает, кому доступны методы полей REST 3.0. В 26.1100.0 встройки модулей без приложения из разбора 26.900.0 открываются в стандартном интерфейсе. До обновления проверьте свои точки входа REST, наследников CRestServer и провайдеры встроек.
Новое
Закройте методы полей REST 3.0 своей политикой (26.1000.0)
Для каждого ресурса REST 3.0 сам генерирует методы полей: *.field.list/get, *.field.custom.* и *.field.custom.enum.*. До 26.1000.0 их закрывал только scope. Теперь на DTO ресурса вешается #[FieldAccess(Provider::class)], и провайдер решает, пропустить ли вызов. Он реализует FieldAccessProvider::decide(FieldAccessContext $context): Result. В контексте есть userId, модуль, URI метода, действие (list, get, add, update, delete), entityId пользовательских полей и аргументы экшена. Ошибка в Result превращается в AccessDeniedException, а вызов без авторизованного пользователя до провайдера не доходит.
Провайдер создаётся через new без аргументов, зависимости берите внутри.
<?php declare(strict_types=1);
namespace Vendor\Kb\Infrastructure\Rest\Access;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\Main\UserGroupTable;
use Bitrix\Rest\V3\Access\FieldAccessContext;
use Bitrix\Rest\V3\Access\FieldAccessProvider;
final class ArticleFieldAccess implements FieldAccessProvider
{
private const ADMIN_GROUP_ID = 1;
public function decide(FieldAccessContext $context): Result
{
$result = new Result();
if (in_array($context->action, ['list', 'get'], true) || $this->isAdmin($context->userId)) {
return $result;
}
return $result->addError(new Error('Поля статей меняют только администраторы', 'VENDOR_KB_FIELDS_READONLY'));
}
private function isAdmin(int $userId): bool
{
return UserGroupTable::getCount(['=USER_ID' => $userId, '=GROUP_ID' => self::ADMIN_GROUP_ID]) > 0;
}
}
use Bitrix\Rest\V3\Attribute\FieldAccess;
use Bitrix\Rest\V3\Dto\Dto;
use Vendor\Kb\Infrastructure\Rest\Access\ArticleFieldAccess;
#[FieldAccess(ArticleFieldAccess::class)]
class ArticleDto extends Dto
{
public int $id;
public string $title;
}
Если модуль уже объявил свой UserFieldAccess в секции userField файла .settings.php, подойдёт готовый UserFieldAccessProvider. Модулю без такого ACL он отказывает с кодом USER_FIELD_ACCESS_NOT_DECLARED. Ресурс без атрибута по-прежнему проверяется только по scope.
Считайте приложения одним вызовом (26.1000.0)
ApplicationProvider получил getCount(), getCodes() и hasAny(). Все три принимают ApplicationFilter: только установленные, внешние атрибуты приложения («код → значение»), источник ApplicationOrigin::Local или Marketplace и excludeImmune, который убирает приложения из списка Immune Маркета. getCodes() выбирает коды пачками по 500 и убирает дубли, getCount() кэширует ответ на cacheTtl секунд.
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\Rest\Public\Provider\ApplicationProvider;
use Bitrix\Rest\Public\Provider\Params\ApplicationFilter;
use Bitrix\Rest\Public\Provider\Params\ApplicationOrigin;
Loader::requireModule('rest');
$apps = new ApplicationProvider();
$market = new ApplicationFilter(installed: true, origin: ApplicationOrigin::Marketplace, excludeImmune: true);
if ($apps->hasAny($market)) {
$count = $apps->getCount($market, cacheTtl: 86400);
$codes = $apps->getCodes($market);
}
Замерьте время REST-методов (26.1100.0)
После каждого запроса LoadLimiter пишет в PSR-логгер rest.LoadLimiter.time строку на пару «вебхук или приложение + метод»: домен, число вызовов, полную длительность и время, которое учёл лимитер. Считаются только вызовы через входящие вебхуки и OAuth-приложения. Пока логгера нет в секции loggers файла bitrix/.settings.php, в лог ничего не попадает. Пример секции:
'loggers' => [
'value' => [
'rest.LoadLimiter.time' => [
'className' => \Bitrix\Main\Diag\FileLogger::class,
'constructorParams' => ['/var/log/bitrix/rest-time.log', 10 * 1024 * 1024],
],
],
'readonly' => true,
],
Нужен включённый лимитер. В облаке он работает всегда, на коробке его включает опция rest / load_limiter_active = Y. Та же опция включает и сам лимит в 420 секунд работы метода за 10 минут на вебхук или приложение (на коробке порог задаёт load_limiter_second_limit). На коробке время между запросами копится в Redis из опции load_limiter_connection_name. Без него лимит фактически не срабатывает, а лог пишется всё равно.
Откройте встройку модуля в интерфейсе портала (26.1100.0)
В 26.900.0 модуль мог привязать встройку к своей сущности, но показывать её приходилось самому. Теперь часть работы берёт rest:
EmbeddingProvider::resolveOpen($embedding, $userId)проверяет доступ пользователя и спрашивает у провайдера владельца, что открыть. Нет доступа или провайдера — вернётсяnull.EmbeddingMenuProvider::getLeftMenuItems($placement)отдаёт пункты левого меню вместе со встройками модулей,getBindingItems($placements)отдаёт пункты меню привязок, там только приложения.- У
EmbeddingOpenDtoпоявилсяmethod:HttpMethod::Getкладёт payload в query,HttpMethod::Postотправляет его формой. - После привязки встройки модуля приходит событие
onAfterPlacementAdd::<PLACEMENT>, как у приложений.
С флагом LegacyNonAppEmbeddings (механизм Main\Config\Feature, по умолчанию выключен) встройки модулей попадают в PlacementTable::getHandlersList(), а bitrix:app.layout открывает их по отрицательному ID. Iframe грузится с хоста портала, и часть JS SDK встройке закрыта: setTitle, openApplication, refreshAuth, setUserOption, setAppOption, setInstall, setInstallFinish, showAppForm.
Провайдер из прошлого разбора теперь выглядит так. Handler в BindEmbeddingCommand остаётся абсолютным http(s)-URL, а открывать нужно путь на портале:
<?php declare(strict_types=1);
namespace Vendor\Kb\Integration\Rest;
use Bitrix\Rest\Public\Dto\Embedding\EmbeddingOwnerDto;
use Bitrix\Rest\Public\Enum\HttpMethod;
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: '/local/kb/embed.php',
payload: ['ARTICLE_ID' => (int)$embedding->entityId],
method: HttpMethod::Post,
),
$embeddings,
);
}
}
Подберите порог потокового ответа (26.1000.0)
Опция rest / streamed_json_commit_threshold задаёт, сколько байт тела копить до первой отправки клиенту. По умолчанию 262144, то есть 256 КБ. Если кодирование JSON упало до порога, клиент получит обычную ошибку WRONG_ENCODING. После первой отправки ответ уже не заменить: поток оборвётся, а в лог через AddMessage2Log уйдёт запись «Rest streamed response».
Чем выше порог, тем больше памяти держит запрос и тем реже клиент видит оборванный ответ.
Что сломается
Проверьте свою точку входа REST (26.1000.0)
CRestServer::output() возвращает Main\HttpResponse|string|null вместо BFile|string|null, а protected outputJson() отдаёт StreamedJsonResponse вместо строки.
- Наследник
CRestServerсoutputJson(): stringупадёт на несовместимой сигнатуре. echo $server->output($result)в своей точке входа падает: уHttpResponseнет__toString(). Делайте как штатныйrest.server: дляHttpResponseвызовитеsendHeadersAdditional()иsend(), для строки оставьтеsendHeaders()иecho. Строку для внутреннего клиента соберётgetContent().send()сбрасывает все буферы вывода и сам завершает запрос, доCMain::FinalActions()дело не доходит. Обработчик, который правил тело REST-ответа через буфер или эпилог, его больше не увидит. Заголовки и куки из ответа контекста переносятся.Content-Lengthу JSON-ответов нет. Проверьте прокси и клиентов, которые на него рассчитывают.
Уберите Vibe+ из своего кода (26.1000.0)
Удалены публичные классы Vibe+ из 26.650.0: Public\Service\VibePlusUsageProvider, VibePlusMarketApplicationLimitProvider, Public\ValueObject\VibePlusUsageSnapshot, VibePlusMarketApplicationLimit и enum Public\Enum\VibePlus\MarketApplicationLimitState. Тариф и лимит приложений теперь считают модули bitrix24 и market, прямой замены в rest нет. Подсчёт приложений Маркета переведите на ApplicationProvider (пример выше), а hasUserRestUsage() на IntegrationProvider::hasUserIntegrations(['vibecodeconnector' => 'Y']). Без аргумента метод учтёт и интеграции приложений с внешним атрибутом vibecodeconnector = Y, которые старый метод пропускал.
Вместе с ними ушли:
- 5-й параметр
$skipTariffCheckуPreset\Provider::saveIntegration(), вызов с именованнымskipTariffCheck:упадёт; $skipTariffCheckу protectedAbstractCreateIncomingWebhookCommandHandler::createIntegration(). На его место встал$dateExpire, и позиционный вызов из наследника получитTypeError;- свойство
excludeAttributesуPublic\Provider\Params\IncomingWebhookFilter.
Обновите провайдеры встроек (26.1100.0)
EmbeddingViewDtoизEmbeddingProvider::getListByPlacement()вместоurlиpayloadнесётEmbeddingOpenDto $open. Читайте$view->open->urlи$view->open->payload.- Конструктор
EmbeddingOpenDtoбросаетInvalidArgumentException, еслиurlне начинается с одного/(абсолютный адрес,//,\, управляющие символы и пробелы) илиpayloadне плоская карта «строка → скаляр». Провайдер с абсолютным handler вurl, как в примере из 26.900.0, теперь бросает это исключение.getListByPlacement()его ловит: встройки такого владельца пропадают из списка, а ошибка уходит в лог.app.layoutвместо встройки показывает «Не удалось открыть встройку», а прямой вызовresolveOpen()получит исключение. BindEmbeddingCommandне привяжет встройку модуля к плейсментам только для приложений:USERFIELD_TYPE,PAGE_BACKGROUND_WORKER,BI_ANALYTICS_MENU,CONTACT_CENTER.run()броситMain\Command\Exception\CommandException, а вgetPrevious()будетRest\Exceptions\ArgumentException«The placement does not support module embeddings».- С включённым
LegacyNonAppEmbeddingsgetHandlersList()отдаёт встройки модулей строками сAPP_ID = -<ID встройки>,INSTALLED = Yи пустымAPP_ACCESS, аgetAccessibleHandlerIds()добавляет их ID к ID встроек приложений. Если ваш код идёт сAPP_IDиз списка обработчиков вAppTable, проверяйтеAPP_ID > 0. embeddingListActionконтроллераBitrix\Rest\Controller\Applicationотдаёт не-администратору пустой список, если тот спросил встройки чужогоuserId. Раньше так ограничивали только внешних пользователей.
Пересмотрите запросы к REST 3.0 (26.1000.0)
- Тело запроса больше не перекрывает параметры, зашитые в маршрут. У
*.field.custom.*и*.field.custom.enum.*маршрут несётentityId, и подменить телом сущность пользовательских полей больше нельзя. *.field.custom.enum.get/update/deleteна значение списка чужой сущности отвечаютEntityNotFoundException, как на несуществующее. Раньше get и update этого не проверяли.fieldsв теле не массивом даётInvalidRequestFieldTypeException.
Мелочи и находки
- В ответе
*.field.list/getREST 3.0 появилосьeditableGroups, аeditableсчитается какeditableGroups !== null. Раньше маппер читал ключeditable, которого в данных нет (26.1000.0). rest.scope.listи OpenAPI-документация больше не показывают выключенные методы (26.1000.0).- Сгенерированные DTO REST 3.0 грузятся лениво. Если серверный код берёт описание метода из
SchemaManager::getMethodDescription()и сразу работает с его DTO, сначала вызовите$schemaManager->ensureGeneratedDtoClassesLoadedFromMethodDescriptions([$description])(26.1000.0). - Из
PlacementTableудалены protected staticgetPlacementCacheId()иgetAccessibleHandlerCacheId(), код переехал во внутреннийHandlerReader. Заденет только наследниковPlacementTable(26.1100.0).
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
Bitrix\Rest\Public\ValueObject\VibePlusMarketApplicationLimit
|
Удалено | — |
Bitrix\Rest\Public\Service\VibePlusUsageProvider
|
Удалено | — |
Bitrix\Rest\Public\Service\VibePlusUsageProvider::getMarketApplicationCount()
|
Удалено |
Bitrix\Rest\Public\Provider\ApplicationProvider::getCount()
|
Bitrix\Rest\Public\Service\VibePlusUsageProvider::getMarketApplicationCodes()
|
Удалено |
Bitrix\Rest\Public\Provider\ApplicationProvider::getCodes()
|
Bitrix\Rest\Public\Service\VibePlusUsageProvider::hasVibecodeUsage()
|
Удалено |
Bitrix\Rest\Public\Provider\ApplicationProvider::hasAny()
|
Bitrix\Rest\Public\Service\VibePlusUsageProvider::hasUserRestUsage()
|
Удалено |
Bitrix\Rest\Public\Provider\IntegrationProvider::hasUserIntegrations()
|
Bitrix\Rest\Public\Service\VibePlusMarketApplicationLimitProvider
|
Удалено | — |
Bitrix\Rest\Public\ValueObject\VibePlusUsageSnapshot
|
Удалено | — |
Bitrix\Rest\Public\Enum\VibePlus\MarketApplicationLimitState
|
Удалено | — |
Bitrix\Rest\PlacementTable::getPlacementCacheId()
|
Удалено | — |
Bitrix\Rest\PlacementTable::getAccessibleHandlerCacheId()
|
Удалено | — |
Bitrix\Rest\Public\Provider\Params\IncomingWebhookFilter::$excludeAttributes
|
Удалено | — |
Bitrix\Rest\Public\Provider\Dto\Embedding\EmbeddingViewDto::$url
|
Удалено |
Bitrix\Rest\Public\Provider\Dto\Embedding\EmbeddingViewDto::$open
|
Bitrix\Rest\Public\Provider\Dto\Embedding\EmbeddingViewDto::$payload
|
Удалено |
Bitrix\Rest\Public\Provider\Dto\Embedding\EmbeddingViewDto::$open
|