Skip to content

Web API: CORS OPTIONS без сессии MODX в БД - #725

Merged
biz87 merged 1 commit into
betafrom
fix/issue-720-options-no-session
Sep 15, 2026
Merged

biz87 merged 1 commit into
betafrom
fix/issue-720-options-no-session

Conversation

@Ibochkarev

Copy link
Copy Markdown
Member

Описание

Каждый CORS OPTIONS к api.php поднимал modX::initialize('web') с сессией. Браузер не шлёт cookie на preflight, поэтому каждый запрос писал новую строку в modx_session.

Для OPTIONS инициализация идёт с session_enabled => false. До этого снимаются $_GET['preview'] и $_REQUEST['preview'], иначе ядро всё равно стартует сессию. Логика вынесена в WebApiModxBootstrap. CORS-контракт роутера и middleware не менялся.

Тип изменений

  • Исправление бага (non-breaking change)
  • Новая функциональность (non-breaking change)
  • Breaking change (изменение, ломающее обратную совместимость)
  • Рефакторинг (без изменения функциональности)
  • Документация
  • Другое (опишите):

Связанные Issues

Closes #720

Связано с #634 / #637 / #706 / #708 / #718.

Как это было протестировано?

php -l assets/components/minishop3/api.php
php -l core/components/minishop3/src/Services/Api/WebApiModxBootstrap.php
# exit 0

cd core/components/minishop3
php tests/ApiPhpOptionsSessionSmokeTest.php
# OK: ApiPhpOptionsSessionSmokeTest

./vendor/bin/phpunit tests/Unit/Services/Api/WebApiModxBootstrapTest.php
# OK (3 tests, 9 assertions)

composer test:smoke
# OK smoke tests (103)

Ручная проверка счётчика сессий (staging, session_handler_class = modSessionHandler):

SELECT COUNT(*) FROM modx_session;
-- N × OPTIONS /api/v1/... с Origin + Access-Control-Request-Method
SELECT COUNT(*) FROM modx_session;
-- должно совпасть

В ответе preflight не должно быть Set-Cookie: PHPSESSID=….

  • Ручное тестирование (рецепт COUNT выше; полный live COUNT — на staging)
  • Автоматические тесты (composer test:smoke, unit bootstrap)
  • Тестирование на разных версиях PHP/MODX

Конфигурация тестирования:

  • MiniShop3: ветка fix/issue-720-options-no-session от beta
  • MODX: 3.x (контракт session_enabled / preview в _initSession)
  • PHP: 8.4.23

Скриншоты (если применимо)

До После
n/a n/a

Чеклист

  • Код соответствует стилю проекта
  • Добавлены/обновлены комментарии в сложных местах
  • Изменения не ломают существующую функциональность
  • Лексиконы добавлены на двух языках (ru/en) — не требуются
  • PHPStan проходит без новых ошибок (composer stan / CI job PHPStan)
  • ESLint проходит без ошибок (npm run lint:ci для Vue) — Vue не трогали
  • Обновлён CHANGELOG.md (для значимых изменений) — на релизе

Дополнительные заметки

  • GET/POST и остальной Web API по-прежнему стартуют сессию как раньше.
  • Автоматика покрывает контракт бутстрапа. Факт «N OPTIONS = 0 новых строк» проверяется COUNT на staging.
  • Три падения HeadlessStorefrontErrorsTest воспроизводятся на чистом origin/beta и к этому PR не относятся.

@Ibochkarev Ibochkarev added priority: medium Средний приоритет bug Something isn't working labels Sep 14, 2026
@Ibochkarev
Ibochkarev requested a review from biz87 September 14, 2026 02:06
@biz87

biz87 commented Sep 14, 2026

Copy link
Copy Markdown
Member

Проверил на живом MODX 3 (dev-сайт, session_handler_class = modSessionHandler). Сам preflight без сессии работает: 5 × OPTIONS не добавляют строк в modx_session, Set-Cookie из ответа ушёл, CORS-заголовки те же. Но опция session_enabled => false в initialize() отключает сессии всему контексту web, если preflight пришёл на холодный кэш. Возвращаю.

Блокер: опция попадает в кэш контекста

modX::initialize($contextKey, $options) передаёт $options в _initContext(), дальше modContext::prepare() и modCacheManager::generateContext(), где

$results['config'] = array_merge($results['config'], $options);

— и результат пишется в core/cache/context_settings/web/context.cache.php. Если кэш контекста пуст (после очистки кэша, сохранения ресурса, установки пакета) и первым пришёл OPTIONS, в кэш записывается 'session_enabled' => false, и дальше все запросы контекста web идут без сессии MODX — пока кэш снова не очистят. Так в 3.1.2-pl и 3.2.4-pl одинаково.

Воспроизведение на dev с файлами PR:

Шаг Результат
очистить кэш → GET /api/v1/cart/get строка в modx_session есть
очистить кэш → OPTIONS /api/v1/cart/add → GET /api/v1/cart/get cookie выдан, строки в modx_session нет
то же, но затем GET / (обычная страница сайта) cookie сессии не выдаётся вовсе
context.cache.php после холодного OPTIONS содержит 'session_enabled' => false

То есть на проде один preflight после сброса кэша ломает сессии — авторизацию покупателей, всё, что хранится в $_SESSION, — для всего сайта до следующей очистки.

Что предлагаю

Не передавать ничего через опции initialize(). Ядро не стартует сессию, если getSessionState() вернул SESSION_STATE_EXTERNAL, а это происходит при isset($_SESSION) (modX::getSessionState(), одинаково в 3.1.2 и 3.2.4). Достаточно перед initialize('web'):

if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) === 'OPTIONS') {
    $_SESSION = [];
}
$modx->initialize('web');

Проверка состояния стоит внутри условия про preview, так что снимать $_GET['preview'] тоже не нужно. Попробовал этот вариант на том же dev-сайте с холодным кэшем:

  • 5 × OPTIONS, OPTIONS ?preview=1, OPTIONS на несуществующий маршрут — modx_session +0, Set-Cookie нет, CORS-заголовки те же;
  • в context.cache.php после холодного OPTIONS нет session_enabled;
  • GET /api/v1/cart/get и GET / после этого пишут сессию как обычно.

Заодно отпадает перенос require_once автозагрузчика до initialize(): класс для этого не нужен, и порядок регистрации автозагрузчиков остаётся как в beta.

Тест

Текущие тесты проверяют, что initializeOptions() возвращает session_enabled => false, и такую поломку не видят. Нужен тест, который ловит отравление кэша: например, live-тест, который очищает кэш контекста, проходит bootstrap для OPTIONS, а затем проверяет, что в сгенерированном конфиге контекста нет session_enabled и что следующий не-OPTIONS bootstrap стартует сессию.

Mark $_SESSION before initialize so getSessionState is EXTERNAL.
Do not pass session_enabled into initialize — it is merged into
context.cache.php on a cold cache and disables sessions site-wide.
@Ibochkarev
Ibochkarev force-pushed the fix/issue-720-options-no-session branch from 5eda377 to 577e724 Compare September 15, 2026 01:22
@Ibochkarev

Copy link
Copy Markdown
Member Author

@biz87 Спасибо — блокер подтверждён. Tip перебазирован на beta и переписан.

Исправление: перед initialize('web') на OPTIONS только $_SESSION = [] (SESSION_STATE_EXTERNAL). Второй аргумент в initialize() не передаём — session_enabled в опциях больше нет. Порядок: лёгкий require_once хелпера → initialize('web') → composer autoload (как на beta).

Тесты: smoke + unit на $_SESSION / отсутствие session_enabled =>; live WebApiOptionsSessionLiveTest — refresh context cache после OPTIONS-prepare и проверка, что в context.cache.php нет session_enabled === false, плюс документация merge-poison.

Smoke + unit — ok.

@biz87
biz87 merged commit 360f02e into beta Sep 15, 2026
6 checks passed
@Ibochkarev
Ibochkarev deleted the fix/issue-720-options-no-session branch September 16, 2026 06:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority: medium Средний приоритет

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Каждый CORS preflight создаёт сессию MODX в базе

2 participants