AI Frontier

CORS и предполёт OPTIONS на шлюзе OpenClaw в macOS в 2026: разрешённые источники, учётные данные, периферийные заголовки nginx и прогон на облачном Mac mini

MacHTML Lab2026.05.15около 33 мин чтения

Когда продуктовые команды подключают браузерную панель или внутреннее SPA к шлюзу OpenClaw, слушающему TCP 8787 в 2026 году, первая «таинственная авария» редко бывает в API модели — чаще это CORS. curl отвечает, Safari и Chrome показывают blocked by CORS policy, потому что браузер отправил предполёт OPTIONS, который периметр не обработал, или потому что Access-Control-Allow-Origin равен *, а fetch использует credentials: 'include'. Этот runbook даёт явные списки Origin, различие простых и запросов с учётными данными, размещение заголовков nginx и ловушку дубликатов, а также воспроизводимые зонды curl для тикетов — в связке с первым запуском PATH, Node и дымом LaunchAgent, занятостью порта и выравниванием health-проб и диагностикой doctor шлюза.

Вы получите матрицу решений, рецепты заголовков, числовые ограничители (204 или 200 для OPTIONS допустимы; при учётных данных без звёздочки) и ориентир аренды около 16,9 $ в сутки на опубликованных страницах MacHTML, чтобы репетировать тот же Safari, что у заказчиков.

Чем браузер отличается от curl

curl https://gateway.example/health доказывает достижимость, но не политику браузера. Пользовательские агенты добавляют Origin при кросс-сайтовых вызовах, могут превратить простой GET в POST с предполётом при появлении нестандартных заголовков и не отдают тело ответа JavaScript, если Access-Control-Allow-Origin не совпадает с исходным Origin (или не * вне сценария без учётных данных). Считайте CORS контрактом двух команд: фронтенд владеет точными строками Origin; платформа владеет порядком заголовков на прыжке, где завершается TLS.

Зафиксируйте каждого вызывающего со схемой, хостом и портом — https://app.corp.internal:8443 не то же самое, что https://app.corp.internal. Пропуск порта в белом списке даёт прерывистые сбои только на staging с нестандартным TLS-портом. Поэтому команды репетируют на выделенном Mac mini с тем же Safari, что на ноутбуках руководства.

Простые запросы и запросы с учётными данными

Простые GET без пользовательских заголовков могут пропустить предполёт, но для чтения тела кросс-origin JavaScript всё равно нужен корректный Access-Control-Allow-Origin. Как только появляются Authorization, Cookie или fetch(..., { credentials: 'include' }), спецификация требует явного эха Origin — подстановка * запрещена — плюс Access-Control-Allow-Credentials: true.

Если отдаёте только анонимные метрики, можно держать * и не использовать cookie. Смешанные режимы — часть маршрутов с учётными данными, часть анонимных — разделяйте блоками location в nginx, чтобы маркетинговые пиксели не унаследовали админские CORS-заголовки по ошибке.

Механика предполёта OPTIONS

Предполёт шлёт OPTIONS с Access-Control-Request-Method и опционально Access-Control-Request-Headers. Шлюз должен ответить Access-Control-Allow-Methods со списком глаголов (обычно GET,POST,OPTIONS), Access-Control-Allow-Headers с эхом имён без учёта регистра и Access-Control-Max-Age для кэша рукопожатия в браузере — в разработке часто 300 секунд, в продакшене 600–86400 в зависимости от ротации заголовков.

204 без тела или 200 с пустым телом оба приемлемы. Типичные провалы: 405 Method Not Allowed, когда nginx направляет OPTIONS не на тот upstream, или отсутствие Vary: Origin при динамическом эхе — CDN кэширует неверный ACAO для всех арендаторов.

Списки Origin и ловушка localhost

http://localhost:5173 (Vite) и http://127.0.0.1:5173 — разные источники. Добавьте оба в локальной разработке или стандартизируйте dev-команду на одно имя хоста. При SSH-локальном форварде 127.0.0.1:8787 Origin браузера остаётся публичным URL SPA — белый список должен включать Origin SPA, а не только http://127.0.0.1.

Динамическое отражение любого входящего Origin в Access-Control-Allow-Origin удобно, но опасно без серверной проверки по фиксированному множеству. Лучше таблица соответствия: если Origin в множестве S — вернуть его; иначе опустить заголовок и дать браузеру fail-closed. Отклонённые Origin логируйте уровнем INFO с сэмплированием для аудита без переполнения дисков.

nginx и дублирующиеся заголовки

Когда nginx завершает TLS и проксирует на OpenClaw по loopback, решите, где живёт CORS — в nginx или в Node. Если в обоих местах, появятся дубли, которые часть браузеров отвергнет. Прагматичное разделение: nginx для статики и маркетинговых страниц; OpenClaw для API, если шлюз уже отдаёт заголовки для CLI. Зафиксируйте выбор в том же runbook, что и graceful shutdown, чтобы дежурные не «чинили» CORS дважды.

При завершении HTTP/2 на nginx убедитесь, что add_header покрывает и ошибочные пути — старые конфигурации без always цепляют заголовки только к 2xx. OPTIONS, попавшие в неверный блок server, — вторая по частоте причина загадочных SPA после несоответствия wildcard и учётных данных.

Vary: Origin, кэш CDN и устаревший ACAO

Когда вы эхом возвращаете запрошенный Origin, ответы зависят от заголовка Origin запроса. Без Vary: Origin промежуточные кэши могут отдать Access-Control-Allow-Origin клиента A сессии клиента B — это и тонкая утечка данных, и функциональный баг. Некоторые CDN агрессивно нормализуют ответы и снимают Vary; откройте тикет у провайдера, если граничные правила переписывают CORS.

На время отладки CORS держите короткий TTL (60 секунд или меньше) для кэшируемых тел ошибок, чтобы плохой выкат не застревал на периметре часами. Связывайте сброс кэша с корреляционными ID в структурированных логах, чтобы сопоставлять HAR с журналами шлюза, даже если пользователь замазывает скриншоты.

Пользовательские заголовки, JWT и CORS маршрутов инструментов

Маршруты инструментов OpenClaw часто добавляют заголовки вроде X-Request-Id или X-OpenClaw-Profile. Любой заголовок вне безопасного списка браузера вызывает предполёт — перечисляйте их явно в Access-Control-Allow-Headers; не полагайтесь на * при учётных данных. Если JWT идёт в Authorization: Bearer, убедитесь, что и предполёт, и фактический POST возвращают одинаковые строки ACAO; иначе появится известная ошибка «нет Access-Control-Allow-Origin», хотя POST на сервере прошёл.

При ротации ключей подписи временно удвойте окно кэша предполёта, чтобы браузеры подхватили новые имена заголовков за 15 минут, затем снова ужмите max-age и занесите в тот же операционный календарь, что и продление TLS.

Матрица решений

ВызывающийС учётными данными?Стратегия ACAOЗаметки
Публичный сайт документацииНет* или статический списокОставаться анонимным; без cookie
Внутреннее админ-SPAДаЯвный эхо-OriginС Allow-Credentials: true
Мобильный WebViewИногдаКастомные схемы OriginПроверять шаблоны URL WebView
Встраивание стороннего SaaSРедкоСтатический партнёрский OriginТолько договорной белый список

Зонды curl для архива

# Предполёт (заменить хост и путь)
curl -i -X OPTIONS "https://gw.example/v1/chat" \
  -H "Origin: https://app.example" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

# Простой GET с Origin для проверки ACAO на GET
curl -i "https://gw.example/health" \
  -H "Origin: https://app.example"

Сохраняйте полные заголовки ответа в тикете; сравнивайте после каждого выката. Если p95 латентности OPTIONS на периметре превышает 150 ms, а GET health остаётся около 12 ms, сначала проверьте маршрутизацию на холодный upstream, а не вините сразу сам OpenClaw.

Чеклист выката

  1. Перечислите все Origin продакшн- и staging-SPA со схемой, хостом и портом.
  2. Назначьте единственного владельца CORS-заголовков — nginx или OpenClaw — и уберите дубликаты.
  3. Гоняйте OPTIONS в CI с теми же строками Origin, что и локально.
  4. Автотестами подтвердите, что сценарии с учётными данными отвергают wildcard ACAO.
  5. После смены порта или заголовков снова выполните openclaw doctor; health остаётся 200.
  6. Снимите HAR Safari и Chrome с арендованной mini и храните 90 дней.

Информационная безопасность всё чаще требует доказательства, что динамическое отражение Origin ограничено; держите файл белого списка в Git с CODEOWNERS у платформенной команды, чтобы «временные» коммиты со звёздочкой не превратились в постоянный вектор атаки.

FAQ

Почему Safari падает, а Chrome нет?

Safari строже к дублирующимся CORS-заголовкам и смешанному контенту; тестируйте оба движка на реальном железе macOS.

Кэшировать предполёт навсегда?

Нет — используйте ограниченный Access-Control-Max-Age, чтобы ротация заголовков распространялась за минуты или часы, а не недели.

Те же правила CORS для WebSocket?

Рукопожатие — это HTTP-upgrade с заголовком Origin; проверяйте ту же логику белого списка, что и для REST.

Аренда Mac mini на Apple Silicon даёт тот же стек Safari и WebKit, на котором руководство демонстрирует UI шлюза. Облачные узлы MacHTML сочетают SSH для скриптовых наборов curl и опциональный VNC, когда дизайнеры смотрят панель Network вживую. Потребление в простое часто около 12 Вт, поэтому держать репетиционную mini целый спринт обычно дешевле, чем выкатить неверный CORS в прод и откатываться во время совета директоров.

Аренда обходит тяжёлые циклы закупки железа: на опубликованных тарифах примерно 16,9 $ в сутки вместо покупки ещё одной коробки, которая простаивает после интеграции. Когда работа по CORS закончена, остановите инстанс; заголовки остаются в git, а оборудование не амортизируется у вас на балансе 36 месяцев.

Репетиция CORS OpenClaw на реальном macOS-шлюзе

Арендуйте облачный Mac mini, чтобы в Safari проверить OPTIONS, fetch с учётными данными и периферийные заголовки nginx до слияния изменений шлюза.

Mac OpenClaw с готовым CORS
от ~16,9 $/сутки