Настройка веб-слоя
Веб-слой — это часть приложения, которая принимает HTTP-запросы и отдаёт ответы: сервер, маршрутизация, контроллеры. У него есть решения, которые не принадлежат ни одному контроллеру и ни одному маршруту, потому что касаются всего приложения сразу.
Таких решений два вида:
- Как устроен сам сервер — на каком адресе он слушает, сколько процессов держит, сколько памяти отводит на запрос, как долго ждёт медленный обработчик.
- Кому браузер разрешит обращаться — политика кросс-доменных запросов, CORS.
Первое решается один раз при запуске, второе — на каждом запросе.
Проблема. Такие настройки легко расползаются по проекту: порт указан при запуске, число воркеров — в systemd-юните, ограничение памяти — в переменной окружения, CORS — где-то в коде инициализации. Через полгода на вопрос «откуда взялось это значение» уходит больше времени, чем на саму правку. Вдобавок ничто из этого не проверяется: опечатка в имени параметра остаётся опечаткой до самого запуска.
Решение. Winter собирает эти решения в один класс — конфигуратор веб-слоя. Он написан обычным кодом, поэтому его проверяют и типы, и подсказки редактора; лежит в проекте рядом с остальным кодом; и подключается сам — искать место для регистрации не нужно.
Класс-конфигуратор
Достаточно унаследовать WebConfigurerAdapter и переопределить нужные методы:
<?php
namespace Main;
use Flytachi\Winter\Kernel\App\ApplicationArguments;
use Flytachi\Winter\Kernel\App\Config\CorsRegistry;
use Flytachi\Winter\Kernel\App\Config\ServerSettings;
use Flytachi\Winter\Kernel\App\Config\WebConfigurerAdapter;
final class WebConfig extends WebConfigurerAdapter
{
public function configureServer(ServerSettings $server, ApplicationArguments $args): void
{
$server->port($args->int('port', 8000))
->workers(4)
->requestTimeout(30);
}
public function configureCors(CorsRegistry $cors): void
{
$cors->allowedOrigins('https://app.example.com')
->allowedHeaders('Content-Type', 'Authorization')
->allowCredentials();
}
}Регистрировать класс нигде не нужно. Его находит тот же скан, что находит
контроллеры, — значит, имя класса и его расположение произвольны, лишь бы файл
лежал в сканируемом каталоге (не в resources/, storage/ или vendor/).
Что наследовать
Есть два варианта, и разница между ними — в том, заставит ли вас язык реализовать оба метода:
| Наследуете | Что получаете | Когда выбирать |
|---|---|---|
WebConfigurerAdapter |
Оба метода уже реализованы пустыми — переопределяете только нужный | Обычный случай |
WebConfigurer (интерфейс) |
Ничего не реализовано — PHP потребует оба метода | Когда хотите, чтобы про второй нельзя было забыть |
WebConfigurerAdapter — это тот же интерфейс с пустыми заглушками, так что выбор
между ними ни на что не влияет, кроме удобства.
Классы, с которыми вы работаете
В сигнатурах обоих методов встречаются три класса. Знать их устройство необязательно — каждый решает ровно одну задачу:
| Класс | Откуда берётся | Что это |
|---|---|---|
ServerSettings |
Первый аргумент configureServer() |
Настройки сервера |
ApplicationArguments |
Второй аргумент configureServer() |
Разобранная командная строка |
CorsRegistry |
Аргумент configureCors() |
Политика CORS |
ServerSettings — набор параметров запускаемого сервера: адрес, число
процессов, лимиты памяти и времени. Устроен как текучий билдер: каждый метод
возвращает сам объект, поэтому вызовы цепляются в одну строку. Объект приходит уже
заполненным — значениями по умолчанию и тем, что нашлось в переменных окружения,
— так что достаточно поправить нужное, а не описывать всё заново.
ApplicationArguments — то, с чем приложение запустили: php call run --port=9000
превращается в объект, у которого можно спросить значение по имени. Нужен, чтобы
источник настройки выбирали вы: взять порт из флага, из .env или написать числом
в коде — решение остаётся за вами, а не за фреймворком.
CorsRegistry — накопитель политики кросс-доменных запросов. Тоже текучий
билдер, но, в отличие от ServerSettings, приходит пустым: пока не вызван ни один
его метод, CORS-заголовки не отправляются вовсе.
Два метода
Методы отвечают за разные вещи и вызываются в разные моменты:
configureServer() |
configureCors() |
|
|---|---|---|
| Что настраивает | Сам сервер: адрес, процессы, память, пределы | Политику доступа для браузеров |
| Когда вызывается | Один раз при запуске | Один раз при запуске |
| Когда действует | На всё время работы приложения | На каждом запросе |
| Что принимает | ServerSettings и ApplicationArguments |
CorsRegistry |
| Где описан | Настройка сервера — ниже | CORS — ниже |
configureServer(ServerSettings $server, ApplicationArguments $args) —
настраивает процесс, в котором приложение живёт. Выполняется в мастер-процессе, до
того как появятся воркеры, поэтому здесь задаётся то, что потом не изменить: порт,
число процессов, лимит памяти на воркер, максимальный размер запроса, дедлайн
обработки. Второй аргумент даёт доступ к разобранной командной строке php call run, так что источник значения выбираете вы: флаг, .env или константа в коде.
configureCors(CorsRegistry $cors) — описывает, каким сторонним источникам
браузер разрешит читать ответы приложения. Сама политика собирается при запуске, но
применяется на каждом ответе, включая 404 и ошибки.
Не переопределили метод — ничего не произойдёт: сервер поднимется с настройками по умолчанию, а CORS-заголовки просто не будут отправляться.
Несколько конфигураторов
Два контракта ведут себя по-разному, и различие — не в теме, а в арности.
WebConfigurer — сколько угодно классов, где угодно, включая
пакеты. Все получают один и тот же CorsRegistry, вклады
складываются: два класса, каждый со своим allowedOrigins(), дадут общий список
источников. CORS — набор правил, и объединение двух наборов осмысленно.
ServerConfigurer — ровно один, и только в приложении. ServerSettings это один
объект, изменяемый на месте: два вклада не складываются, второй затирает первый.
| Контракт | Кто вправе | Сколько |
|---|---|---|
WebConfigurer → configureCors() |
приложение и пакеты | сколько угодно, складываются |
ServerConfigurer → configureServer() |
только приложение | один |
Сервер настраивает только приложение
ServerConfigurer, найденный внутри импортированного пакета, — ошибка загрузки с именем
класса и пакета:
Only the application may configure the server; found AcmeBillingTuning in acme/billing.Иначе пакет, тюнящий worker_num, тихо переопределял бы выбор приложения — а кто
победит, решал бы порядок обхода файловой системы. Пакету остаётся WebConfigurer: он
может дополнить политику CORS, но не двигать адрес сервера.
Больше одного ServerConfigurer в самом приложении — тоже ошибка: у сервера один хозяин.
Порядок: приложение последнее
Конфигураторы применяются в определённом порядке — сначала пакеты, в порядке объявления
#[Import], затем приложение. Для складывающихся реестров это влияет только на порядок
записей; там, где вклады перекрывают друг друга, приложение выигрывает всегда.
Настройка сервера
configureServer() вызывается один раз при запуске, в мастер-процессе, до того
как появятся воркеры. Всё, что здесь задано, действует до остановки приложения:
поменять адрес или лимит памяти на лету нельзя.
public function configureServer(ServerSettings $server, ApplicationArguments $args): void
{
$server->port($args->int('port', 8000))
->workers(4)
->profile(Profile::Balance)
->requestTimeout(30)
->memoryLimit('256M');
}Вызовы цепляются — каждый метод возвращает сам объект настроек.
Что здесь вообще настраивается
Winter работает на Swoole, и это меняет смысл почти всех привычных цифр. Стоит разобрать модель, иначе непонятно, зачем нужен каждый параметр.
Приложение живёт в нескольких воркерах — отдельных процессах. Каждый воркер поднимает PHP один раз и дальше обрабатывает запросы, не умирая между ними. При этом внутри одного воркера запросы идут не по очереди: каждый выполняется в своей корутине, и пока один ждёт ответа базы, воркер обслуживает остальные.
Отсюда два следствия, определяющих все настройки на этой странице:
- Память общая. Все одновременные запросы воркера живут в одной куче. Запрос, который съест лишнее, убивает не себя, а весь процесс — вместе со всеми запросами, которые тот в этот момент держал.
- Память не освобождается сама. Процесс не завершается после ответа, поэтому всё, что накопилось, остаётся до следующего запроса.
Поэтому настройки сервера — это по сути ответ на один вопрос: сколько запросов воркер может держать одновременно, не рискуя упасть. Дальше — по группам.
Адрес
| Метод | По умолчанию | Назначение |
|---|---|---|
host(string) |
0.0.0.0 |
Сетевой интерфейс, который слушает сервер |
port(int) |
8000 |
Порт |
0.0.0.0 означает «все интерфейсы» — сервер виден и снаружи. Если приложение
стоит за nginx на той же машине, разумно сузить до 127.0.0.1, чтобы порт не
торчал в сеть.
Значения уже заполнены из флагов --host и --port, так что трогать эти методы
нужно, только если вы хотите брать адрес откуда-то ещё.
Процессы
| Метод | По умолчанию | Назначение |
|---|---|---|
workers(int) |
по числу ядер | Сколько процессов обрабатывают запросы |
taskWorkers(int) |
нет | Отдельные процессы под фоновые задачи Swoole |
workers() — это про использование процессорных ядер, а не про параллелизм
запросов. Параллелизм внутри воркера обеспечивают корутины, и один процесс уже
держит сотни запросов сразу. Воркеры нужны, чтобы задействовать все ядра: пока один
процесс считает, остальные работают.
Помните, что лимит памяти умножается на их число — четыре воркера по 256M требуют
гигабайт.
Замена воркеров
| Метод | По умолчанию | Назначение |
|---|---|---|
maxRequest(int) |
из профиля | Сколько запросов отработает воркер, прежде чем его заменят свежим. 0 — никогда |
maxRequestGrace(int) |
десятая часть maxRequest |
Разброс, чтобы воркеры не перезапускались разом |
Долгоживущий процесс накапливает мусор, и от этого есть лекарство: периодически заменять воркер на новый. Но лекарство не бесплатное — замена убивает запросы, которые воркер в этот момент обслуживал, и клиент видит оборванное соединение, хотя в логах не появляется ничего.
Поэтому Winter меряет, а не перестраховывается. Течёт не всякий запрос: замеры
на живом сервере дали ноль прироста кучи на 4,6 млн обычных запросов. Утечка в
180 байт возникает только там, где запрос взводит таймер — Coroutine::sleep()
и всё, что на нём построено. Отсюда порог: воркер заменяется, когда накопленная
утечка могла бы дойти до 20 % его лимита памяти.
maxRequestGrace решает отдельную беду: при ровном трафике воркеры досчитывают до
одного и того же числа почти одновременно и перезапускаются вместе — а вместе с
ними разом холодеют все пулы соединений. Swoole прибавляет к лимиту случайную
величину до grace, и перезапуски расходятся во времени.
Если ваши обработчики никогда не спят, замену можно отключить (maxRequest(0)) —
платить оборванными запросами будет не за что.
Одновременная нагрузка
| Метод | По умолчанию | Назначение |
|---|---|---|
maxConcurrency(int) |
из профиля | Сколько запросов воркер обрабатывает одновременно |
maxConnections(int) |
из профиля | Потолок одновременных TCP-соединений |
idleConnectionTimeout(int) |
выключено | Через сколько секунд молчания закрыть соединение |
maxConcurrency() — главный предохранитель. Каждый запрос в работе занимает
память: около 78 КБ уходит на сам каркас — корутину, объекты запроса и ответа, — и
сверх того столько, сколько выделит ваш код. Когда одновременных запросов слишком
много, воркер упирается в лимит и умирает вместе со всеми ними. Ограничение
превращает эту катастрофу в очередь: лишние запросы просто ждут.
Ожидание в очереди засчитывается в дедлайн запроса, так что подвисший клиент не получит свежие тридцать секунд сверх тех, что уже прождал.
maxConnections() — потолок сокетов. Здесь работает второе ограничение,
никак не связанное с памятью: сокет — это файловый дескриптор, а их число задаёт
ulimit -n. Выведенное из профиля значение Winter подрезает под этот предел, чтобы
объявленное число совпадало с фактическим. Заданное вручную — оставляет как есть:
предупреждение Swoole о превышении дойдёт до того, кто его запросил, и подскажет
поднять ulimit.
idleConnectionTimeout() — про keep-alive. Открытое соединение занимает около
68 КБ, даже когда клиент ничего не просит, и эта память считается в лимит PHP.
Воркер со стандартными 128 МБ умирает примерно на 1900 простаивающих соединениях.
Если клиентов много и они держат соединения подолгу, поставьте таймаут молчания.
Пределы одного запроса
| Метод | По умолчанию | Назначение |
|---|---|---|
maxRequestSize(int $bytes) |
8 МБ | Наибольший запрос вместе с заголовками; сверх — 413 |
requestTimeout(float $sec) |
30 |
Сколько запрос может идти, прежде чем сервер перестанет ждать. 0 — без предела |
maxRequestSize(). Тело запроса лежит в куче воркера всё время обработки, а
куча общая. Поэтому предел здесь не про диск и не про сеть, а про то, сколько
одновременных загрузок переживёт процесс: 64 МБ при сотне параллельных загрузок —
это 6,4 ГБ, и воркер умрёт раньше, чем закончится закачка. Значение по умолчанию
совпадает с post_max_size в PHP, чтобы не удивлять привычных цифр.
requestTimeout(). Своего таймаута запроса у Swoole нет — дедлайн держит
сторож, который отменяет корутину зависшего запроса. Отмена корректная: finally
и defer отрабатывают, транзакции закрываются, соединения возвращаются в пул,
клиент получает 504.
Важно понимать границу: прерывается запрос, который чего-то ждёт — базу, внешний сервис, файл. Запрос, который жжёт процессор в цикле без ввода-вывода, не прервётся, потому что пока он не отпустит поток, в воркере не выполняется ничего, включая сам сторож.
Отдельным маршрутам дедлайн переопределяется атрибутом #[Timeout] — см.
Маршрутизацию.
Память
| Метод | По умолчанию | Назначение |
|---|---|---|
memoryLimit(string) |
ini PHP | Потолок памяти каждого воркера |
memoryTrimThreshold(string) |
из профиля | Сколько простаивающей памяти воркер держит, прежде чем вернуть её системе. 0 — не возвращать |
memoryLimit() — это memory_limit PHP, применяемый при старте каждого
воркера. От него считается почти всё остальное: и допустимый параллелизм, и порог
замены воркера, и порог возврата памяти.
Лимит памяти — на воркер, а не на приложение
memoryLimit('512M') при восьми воркерах означает 4 ГБ в худшем случае. Winter
считает это произведение при старте и предупреждает, если машина столько не держит,
но запуску не мешает: перезаклад бывает осознанным решением.
memoryTrimThreshold() — про возврат памяти операционной системе. После
всплеска нагрузки воркер продолжает удерживать выделенное, хотя оно уже не нужно.
Проверка после каждого ответа возвращает излишек, но операция не бесплатна: когда
памяти много, она занимает около 80 мс и видна в хвосте задержек. Поэтому порог
масштабируется от лимита, а не задан фиксированным числом.
Статика
$server->staticPath('resources/static'); // resources/static/app.css → /app.cssОтдача статики выключена по умолчанию — пока каталог не указан, ни один файл наружу не отдаётся.
Статика идёт мимо PHP
Такие файлы отдаёт сам Swoole, до того как управление попадёт в PHP. Значит, к ним не применяются ни middleware, ни CORS, ни логирование запросов — маршрутизация о них попросту не знает.
Всё остальное
Любой параметр Swoole, для которого нет отдельного метода, задаётся напрямую:
$server->set('open_http2_protocol', true);Профили
Задавать пределы поштучно приходится редко: почти все они выводятся из профиля.
Профиль — это способ не считать вручную взаимосвязанные лимиты. Он отвечает на один-единственный вопрос: сколько памяти расходует один ваш запрос. Из ответа арифметически следует всё остальное — сколько запросов воркер может держать одновременно, сколько соединений открыть, когда возвращать память системе.
$server->profile(Profile::Balance);Все три рабочих профиля одинаково надёжны. Они не торгуют устойчивостью ради скорости — наоборот, каждый существует ровно затем, чтобы воркер не перенапрягал память и не падал, унося с собой обслуживаемые запросы. Различаются они только предположением о размере запроса:
| Профиль | Бюджет запроса | Для каких приложений |
|---|---|---|
Stable |
512 КБ | Тяжёлые запросы: отчёты, выгрузки, широкие выборки |
Balance |
128 КБ | Обычный CRUD — значение по умолчанию |
Performance |
64 КБ | Лёгкие запросы: тонкий API, прокси, интеграционный шлюз |
Бюджет — это память на собственную работу запроса: загруженные сущности, собранная строка ответа. Каркас (около 78 КБ на запрос и 68 КБ на соединение) добавляется поверх и от профиля не зависит.
Меньший бюджет означает больший параллелизм: при одной и той же памяти
Performance пропустит одновременно втрое больше запросов, чем Stable. Поэтому
выбор профиля — не «насколько я осторожен», а «насколько крупные у меня
запросы». Причём это измеряется, а не угадывается:
$before = memory_get_usage();
// ... обработчик ...
$after = memory_get_usage(); // меньше 64 КБ → Performance; больше 200 КБ → StableОшибка в выборе не делает приложение безопаснее: если запросы тяжелее заявленного,
Performance не спасёт — он лишь позволит набрать больше одновременных запросов,
чем воркер выдержит.
`Stress` — только для замеров
Четвёртый профиль, Profile::Stress, единственный снимает ограничители: без
предела параллелизма, без замены воркеров, без возврата памяти. Он существует не
для того, чтобы «выжать ещё» — пропускная способность упирается в потолок задолго
до памяти. Он убирает периодические помехи, искажающие замер: возврат памяти
даёт паузы, видимые в p99, замена воркера опустошает пул соединений на середине
прогона, сторож запросов крутит собственный таймер.
Под этим профилем воркер может исчерпать память, а на долгом прогоне утечка накапливается без замены. Для ограниченного по времени бенчмарка это приемлемо, для продакшена — нет.
Всё, что решает профиль, — это значения по умолчанию. Явный вызов
maxConcurrency(), maxRequest() или переменная окружения перекрывают его вне
зависимости от порядка вызовов.
Настройка через .env
Те же параметры задаются переменными окружения — удобно, когда значения разные на стенде и в проде:
| Переменная | Соответствует |
|---|---|
SERVER_PROFILE |
profile() |
SERVER_WORKERS |
workers() |
SERVER_TASKS |
taskWorkers() |
SERVER_MAX_REQUEST |
maxRequest() |
SERVER_MAX_REQUEST_GRACE |
maxRequestGrace() |
SERVER_MAX_REQUEST_SIZE |
maxRequestSize() |
SERVER_MAX_CONNECTIONS |
maxConnections() |
SERVER_MAX_CONCURRENCY |
maxConcurrency() |
SERVER_IDLE_TIMEOUT |
idleConnectionTimeout() |
SERVER_MEMORY_LIMIT |
memoryLimit() |
SERVER_MEMORY_TRIM |
memoryTrimThreshold() |
SERVER_REQUEST_TIMEOUT |
requestTimeout() |
Приоритет — от явного к выведенному: вызов в configureServer() перекрывает
переменную окружения, переменная перекрывает значение из профиля. Порядок вызовов
при этом не важен — профиль подставляет своё только там, где не сказано ничего.
Как всё это отражается на модели выполнения — корутины, общее состояние, отличия от PHP-FPM — на странице Рантайм.
CORS
CORS решает, каким сторонним источникам браузер разрешит обращаться к вашему API.
Winter даёт два уровня: глобальную политику для всего приложения и
пер-маршрутное переопределение атрибутом #[CrossOrigin].
Что такое CORS и зачем
CORS — Cross-Origin Resource Sharing, «совместное использование ресурсов между источниками». Это механизм, которым сервер разрешает браузеру отдавать ответ коду с другого источника.
Источник (origin) — это тройка «схема + хост + порт»: https://app.example.com
и https://api.example.com — разные источники. По умолчанию браузеры
действуют по правилу same-origin policy: JavaScript со страницы одного
источника не может прочитать ответ от другого. Это защита от того, чтобы чужой сайт
дёргал ваш API от имени пользователя.
Проблема. Типичная связка «SPA + API» живёт на разных источниках: фронтенд на
app.example.com, API на api.example.com. Браузер по умолчанию заблокирует
запрос фронтенда к API — в консоли появится знакомое CORS policy: No 'Access-Control-Allow-Origin' header.
Решение. Сервер должен явно сказать браузеру, каким источникам он доверяет —
через заголовки ответа (Access-Control-Allow-Origin и родственные). Winter
проставляет эти заголовки за вас: глобально для всего приложения и
точечно на конкретном маршруте.
CORS — это про браузер
CORS проверяет браузер, а не сервер. Запросы из curl, Postman или
бэкенд-к-бэкенду ограничение same-origin не касается — заголовки CORS на них не
влияют.
Глобальная политика
Задаётся в configureCors(). CorsRegistry — текучий билдер: каждый метод
принимает список значений через запятую (не массив) и возвращает сам себя, поэтому
вызовы цепляются.
| Метод | По умолчанию | Что делает |
|---|---|---|
allowedOrigins(...$origins) |
пусто | Разрешённые источники |
allowedHeaders(...$headers) |
пусто | Заголовки, которые браузеру можно прислать |
exposeHeaders(...$headers) |
пусто | Заголовки ответа, видимые из JavaScript |
allowCredentials(bool) |
false |
Разрешает куки и Authorization |
maxAge(int $seconds) |
0 |
На сколько браузер закеширует preflight |
vary(...$headers) |
пусто | Дополнительные значения заголовка Vary |
Не тронули ничего — CORS не отправляется вовсе. Пустой configureCors()
равносилен отсутствию конфигуратора: все ответы остаются same-origin.
Как ведут себя настройки
Несколько деталей, которые видно только по итоговым заголовкам.
allowedOrigins работает в трёх режимах — в зависимости от количества:
| Источников | Что уходит в ответ |
|---|---|
| ни одного | Access-Control-Allow-Origin: * |
| ровно один | Этот источник, всегда и без Vary |
| несколько | Совпавший Origin из запроса плюс Vary: Origin |
Третий режим и нужен, чтобы промежуточные кеши не отдали ответ, выписанный одному источнику, другому.
allowedHeaders и maxAge уходят только в preflight. На обычных ответах их
нет — они и не нужны там по спецификации. Если allowedHeaders не задан, фреймворк
отражает то, что браузер запросил в Access-Control-Request-Headers.
allowCredentials() требует явных источников. Спецификация запрещает пару
«* + credentials», и Winter такой заголовок просто не отправит: при пустом
allowedOrigins() вызов allowCredentials() молча ничего не даст. Браузер при
этом никакой ошибки не покажет — запрос с куками просто не пройдёт.
// ✗ заголовок Allow-Credentials не будет отправлен
$cors->allowCredentials();
// ✓ источники перечислены явно
$cors->allowedOrigins('https://app.example.com')
->allowCredentials();vary() заменяет заголовок целиком. Если источников несколько, фреймворк уже
поставил Vary: Origin, а ваш vary() перезапишет его. Указывайте Origin в
списке сами:
$cors->allowedOrigins('https://app.example.com', 'https://admin.example.com')
->vary('Origin', 'Accept-Language');Готовые конфигурации
Три случая, которые покрывают почти всё.
Публичный API только на чтение — доступен откуда угодно, без куки:
$cors->allowedHeaders('Content-Type')
->maxAge(86400);SPA с авторизацией — один известный фронтенд, куки и токены разрешены:
$cors->allowedOrigins('https://app.example.com')
->allowedHeaders('Content-Type', 'Authorization')
->allowCredentials()
->maxAge(3600);Несколько фронтендов — список источников, ответ подстраивается под запрос:
$cors->allowedOrigins(
'https://app.example.com',
'https://admin.example.com',
)
->allowedHeaders('Content-Type', 'Authorization')
->allowCredentials()
->maxAge(3600);Политика на отдельном маршруте
Когда одному эндпоинту нужна политика строже или мягче общей, на контроллер или
метод вешается #[CrossOrigin]. Параметры те же шесть, только именованными
аргументами:
use Flytachi\Winter\Kernel\Route\Annotation\CrossOrigin;
#[CrossOrigin(origins: ['https://admin.example.com'], credentials: true)]
#[RequestMapping('admin')]
class AdminController extends Controller
{
#[GetMapping('stats')]
public function stats(): ResponseEntity { /* admin.example.com */ }
#[GetMapping('feed')]
#[CrossOrigin(origins: ['https://partner.example.com'], maxAge: 3600)]
public function feed(): ResponseEntity { /* partner.example.com */ }
}Приоритет — от частного к общему: метод перекрывает класс, класс перекрывает глобальную политику.
| Аргумент атрибута | Метод билдера |
|---|---|
origins |
allowedOrigins() |
allowHeaders |
allowedHeaders() |
exposeHeaders |
exposeHeaders() |
credentials |
allowCredentials() |
maxAge |
maxAge() |
vary |
vary() |
Заменяет, а не дополняет
#[CrossOrigin] полностью вытесняет глобальную политику для своего маршрута,
а не сливается с ней. Если глобально разрешён заголовок Authorization, а в
атрибуте указаны только origins, то на этом маршруте Authorization окажется
запрещён.
Практическое правило: добавляя #[CrossOrigin], перенесите в него все нужные
параметры из глобальной политики.
Preflight-запросы
Перед «непростым» запросом — с нестандартным методом, заголовком Authorization
или JSON-телом — браузер сначала отправляет OPTIONS и спрашивает разрешение. Этот
запрос фреймворк обрабатывает сам: отвечает 204 с нужными заголовками до
middleware и контроллера. Писать метод под OPTIONS не нужно.
Чтобы ответить правильно, роутер сначала выясняет, какой маршрут собирается вызвать
браузер — по заголовку Access-Control-Request-Method, — и берёт #[CrossOrigin]
именно этого маршрута, если он задан.
Где заголовки появляются
Глобальные заголовки пишутся до поиска маршрута, поэтому они есть на любом
ответе: успешном, 404, 405 и на ошибках 5xx.
Это сделано намеренно. Если бы на 404 заголовков не было, браузер спрятал бы от
JavaScript настоящий статус и показал бы вместо него ошибку CORS — разработчик
искал бы проблему в политике доступа вместо опечатки в адресе.
Дальше
- Рантайм — второй метод конфигуратора: адрес, воркеры, лимиты
- Маршрутизация — где живёт
#[CrossOrigin] - Middleware — своя пред- и постобработка запроса
- Контроллеры — что защищает политика