Если открыть DevTools → Network перед fetch с заголовком Authorization — иногда там перед нашим запросом появляется второй, который мы не отправляли: OPTIONS на тот же URL. Это и есть preflight, «предполётная проверка».
Зачем
Часть запросов считается «опасной»: меняют состояние сервера (DELETE), несут нестандартные заголовки (Authorization, кастомные X-*) или формат тела, отличный от привычных HTML-форм. Перед таким запросом браузер хочет спросить у сервера: «а ты вообще разрешаешь такие запросы с моего origin?».
И отправляет preflight — пустой OPTIONS-запрос на тот же URL, в котором описывает намерение:
OPTIONS /api/posts/42
Origin: https://fruntend.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type
Сервер должен ответить разрешением:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://fruntend.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
Браузер видит разрешение и только после этого отправляет настоящий DELETE. Без разрешения — в JS прилетает CORS-ошибка, настоящий запрос даже не уходит.
Когда preflight не нужен (simple request)
Без preflight браузер шлёт запрос напрямую, если выполняются все условия:
- Метод — GET, HEAD или POST.
- Из заголовков — только «CORS-safelisted»: Accept, Accept-Language, Content-Language, Content-Type с ограниченным набором значений (см. п. 3).
- Если есть Content-Type — одно из: application/x-www-form-urlencoded, multipart/form-data, text/plain.
- Никаких слушателей на upload-событиях.
Чаще всего не попадает в simple request:
- любой PUT, PATCH, DELETE — всегда preflight;
- POST с Content-Type: application/json — preflight (тип JSON не в безопасном списке);
- любой запрос с Authorization или кастомным X-* — preflight.
То есть для большинства настоящих API-запросов preflight будет лететь автоматически.
Заголовки preflight по полочкам
На стороне запроса (отправляет браузер сам):
- Origin — откуда страница.
- Access-Control-Request-Method — какой метод хочется отправить дальше.
- Access-Control-Request-Headers — какие нестандартные заголовки хочется отправить.
На стороне ответа (отвечает сервер):
- Access-Control-Allow-Origin — разрешённый origin (точное значение или *).
- Access-Control-Allow-Methods — какие методы можно.
- Access-Control-Allow-Headers — какие заголовки можно.
- Access-Control-Allow-Credentials — true, если разрешено отправлять куки. Об этом ниже.
- Access-Control-Max-Age — сколько секунд можно кэшировать preflight (об этом ниже).
Кэширование preflight
Без оптимизации каждый защищённый запрос превращается в два: сначала OPTIONS, потом сам запрос. Это удваивает количество ходок в сеть.
Лечится заголовком Access-Control-Max-Age: сервер говорит «разрешение действует столько-то секунд», и браузер запоминает. В этот срок preflight для пары (метод, путь, заголовки) больше не шлётся, идёт только настоящий запрос.
Типичные значения: 3600 (час), 86400 (сутки). Браузеры ограничивают максимум: Chrome — 7200 секунд, Firefox — 24 часа, дальше всё равно скинут.
Это бэковая настройка, фронт на неё не влияет, но знать про существование полезно: если у вас в Network вы видите OPTIONS перед каждым запросом — есть смысл попросить бэкенд добавить Max-Age.
Запросы с куками: credentials: 'include'
По умолчанию fetch на другой origin не отправляет куки. Это ещё один слой защиты: даже если CORS разрешён, куки остаются дома.
Если куки нужны (например, refresh-токен в HttpOnly-куке) — явно просим:
const response = await fetch('https://api.fruntend.com/me', {
credentials: 'include',
});
Тогда браузер пришлёт куки — но только если сервер разрешил это в preflight через два заголовка:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://fruntend.com ← не "*", обязательно точный origin
Звёздочка вместе с Allow-Credentials: true запрещена — иначе любой сайт мог бы дёргать API с куками пользователей. Origin должен быть указан точно.
Резюме главы
- На «сложные» запросы (DELETE, JSON-POST, кастомные заголовки) браузер сам шлёт preflight OPTIONS.
- Сервер должен ответить нужными Access-Control-Allow-*-заголовками, иначе настоящий запрос не уйдёт.
- Access-Control-Max-Age кэширует preflight на стороне браузера — экономит сеть.
- Для запросов с куками нужно credentials: 'include' в fetch и Allow-Credentials: true на сервере.
В следующей главе — что делать, когда CORS всё-таки ругнулся, и как читать его сообщения.