Назад в блог

Корзина в headless Bitrix: пять проблем, которые нас удивили

Каталог мы отдали в Next.js за девять дней. Корзину интегрировали за месяц. Оформление заказа — ещё семь недель.

Потом я посмотрел на дату первого коммита и дату первой успешной продажи через headless-фронт. Разница — два с половиной месяца на то, что на первый взгляд выглядело как «добавить товар, посчитать итог, принять оплату».

Bitrix.Sale — это не API магазина. Это маленькая CRM с собственной логикой, которую REST отдаёт частично и не всегда предсказуемо.

Почему каталог легко, а корзина — нет

Каталог в headless работает предсказуемо: запрашиваешь catalog.product.list, получаешь данные, рендеришь в Next.js. Элемент инфоблока не знает, кто смотрит. Он просто существует.

Корзина устроена иначе. Она привязана к пользователю, к сессии, к истории действий. За ней стоит Bitrix\Sale\Order — объект с состоянием, который живёт в серверном PHP-контексте. REST API только смотрит в это окошко, доступ к объекту напрямую не даётся.

Когда Next.js просит Bitrix добавить товар в корзину, Bitrix должен знать, чья это корзина. Вот тут и начинается.

Проблема 1: сессии в cross-domain архитектуре

Bitrix хранит корзину в серверной сессии. Сессия привязана к cookie PHPSESSID. Когда фронт на shop.example.com делает запрос к backend.example.com/api/ — это cross-domain. Браузер не отправляет cookie без явных заголовков SameSite=None; Secure.

Мы потратили три дня на диагностику ситуации, когда корзина работала в одном браузере и не работала в другом. Оказалось — Safari блокировал сторонние cookies по умолчанию. Корзина исчезала.

Решение простое, но требует понимания: REST-запросы должны идти с credentials: 'include' на фронте, а на бэке — правильный CORS с Access-Control-Allow-Credentials: true и явным списком доменов. Wildcard * с credentials не работает.

Дополнительная сложность: если пользователь авторизован, нужно прокидывать не только сессионный cookie, но и BX_SESSID для CSRF-защиты при мутирующих операциях. Эта связка нигде в официальной документации не объяснена как система.

Проблема 2: sale.order.add и то, чего он не умеет

sale.order.add — эндпоинт для создания заказа через REST. На бумаге он делает всё. На практике он не умеет:

  • применять купоны через параметр напрямую (нужен отдельный вызов sale.basket.addItem с coupon_code, и то не всегда)
  • вычислять финальную цену с учётом персональных скидок пользователя до создания заказа
  • работать с нестандартными обработчиками заказа, добавленными через PHP-события

Мы обнаружили это на проекте, где у клиента была накопительная система скидок, написанная на OnSaleOrderBeforeSaved. REST-эндпоинт эту логику обходил. Заказы создавались с ценой без скидки. Три дня отладки, пока не нашли место. (В предыдущей статье про сюрпризы Bitrix REST API мы разбирали offset-pagination и OAuth — это другой класс проблем, но корень тот же: REST API не раскрывает весь внутренний контракт PHP-слоя.)

Если у вас нестандартная бизнес-логика в Sale-модуле — проверьте её совместимость с REST до начала headless-проекта. Не после.

Проблема 3: купоны и персональные скидки

В монолитном Bitrix это работает через серверный контекст: пользователь вошёл, система знает его группу, и скидки применяются при оформлении сами.

В headless REST API ситуация другая. Персональная скидка применяется, если REST-запрос приходит от авторизованного пользователя с корректной сессией. Но «корректная сессия» в REST-контексте — это не то же самое, что авторизация на сайте.

На практике: если пользователь вошёл через кастомный механизм авторизации (а у большинства наших клиентов он именно кастомный), REST API может «не видеть» группы пользователя и не применять скидки.

Мы в итоге вынесли расчёт финальной цены на PHP-прокси-эндпоинт, который живёт на том же сервере что и Bitrix, имеет прямой доступ к PHP API и возвращает уже посчитанную цену. Это не идеально с точки зрения архитектуры headless, но это работает.

Проблема 4: службы доставки и их виджеты

СДЭК, Почта России, PickPoint — у всех есть виджеты для выбора пункта выдачи. Все виджеты написаны в расчёте на то, что они встраиваются в PHP-страницу Bitrix.

Виджет СДЭК ожидает серверный рендеринг и наличие определённых PHP-переменных в контексте страницы. В Next.js этого контекста нет. Виджет просто не инициализируется.

Решение: встраивать виджеты через iframe на выделенные PHP-страницы Bitrix, которые остаются в монолитной части, и передавать выбранный пункт через postMessage в Next.js. Некрасиво. Зато работает.

Проблема 5: платёжные шлюзы

Большинство платёжных шлюзов, подключённых к Bitrix, работают по redirect-flow: пользователь нажимает «Оплатить», Bitrix формирует ссылку и редиректит на страницу шлюза, шлюз после оплаты редиректит обратно на Bitrix-страницу подтверждения.

В headless-архитектуре обратный редирект попадает на PHP-страницу Bitrix, а не в Next.js. Это ломает UX: пользователь оказывается на старом шаблоне Bitrix после оплаты.

Решений два:

  • Оставить страницу подтверждения оплаты на Bitrix-шаблоне (мы так и сделали — это быстрее)
  • Настроить кастомный redirect с Bitrix-страницы на Next.js с передачей order ID и статуса

Шлюзов с полноценным API-flow (без редиректа) в экосистеме Bitrix немного. ЮKassa умеет — но требует отдельной реализации обработчика webhook.

Что мы оставили на PHP и почему

По итогу нашего проекта checkout живёт в гибридной модели:

  • Корзина (добавление/удаление) — через REST API с cookie forwarding
  • Расчёт итогов и скидок — PHP-прокси на том же сервере
  • Страница оформления заказа — Next.js форма с REST для создания заказа
  • Страница подтверждения оплаты — Bitrix PHP шаблон

Это не чистый headless. Но это работает, приносит деньги, и я не буду делать вид, что было иначе.

Headless Bitrix — это не замена всего PHP одним REST API. Каталог и рендеринг уходят в Next.js. Бизнес-логика Sale-модуля остаётся на PHP. Задача инженера — провести эту границу правильно. Не стереть её. Подробнее о том, что именно мы оставляем на стороне Bitrix и почему — в разборе архитектурных границ.

Мы уложились. Просто дольше, чем думали в начале.