Как браузер решает, пропустить запрос или заблокировать
Когда JavaScript на странице пытается отправить запрос на другой домен, браузер не передаёт его сразу серверу. Вместо этого он проводит серию проверок, чтобы понять, разрешён ли такой запрос. Эти проверки и называются CORS — Cross-Origin Resource Sharing. Механизм работает не как отдельный протокол, а как набор ограничений, которые браузер накладывает поверх HTTP.
Процесс выглядит так:
- Скрипт на странице вызывает
fetch()илиXMLHttpRequestс адресом другого домена. - Браузер анализирует параметры запроса: метод, заголовки, наличие credentials (кук или токенов).
- Если запрос не подходит под критерии простого запроса, браузер сначала отправляет предварительный запрос
OPTIONSна тот же адрес. - Сервер отвечает заголовками, которые описывают, какие запросы он разрешает. Например:
Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: Content-Type, Authorization - Браузер сравнивает эти заголовки с параметрами исходного запроса. Если всё совпадает — браузер пропускает ответ сервера к скрипту. Если нет — блокирует и показывает ошибку в консоли.
Ключевой момент здесь в том, что сервер должен не просто вернуть данные, а явно разрешить доступ к ним. Без этого браузер не отдаст ответ скрипту, даже если сервер его вернул. Это не баг, а защита: браузер не знает, доверяет ли пользователь этому другому домену, поэтому требует явного разрешения.
Какие запросы считаются простыми и почему это важно
Браузер не отправляет предварительный запрос OPTIONS для простых запросов. Простыми считаются те, которые не могут изменить данные на сервере или украсть чувствительную информацию. Критерии простого запроса:
- Методы: только
GET,HEADилиPOST. - Заголовки: только те, которые браузер может добавить автоматически. Это
Accept,Accept-Language,Content-LanguageиContent-Typeс одним из трёх значений:text/plain,multipart/form-dataилиapplication/x-www-form-urlencoded. - Нет пользовательских заголовков (например,
AuthorizationилиX-Custom-Header).
Если запрос не подходит под эти условия, браузер сначала отправит OPTIONS, чтобы узнать, разрешает ли сервер такой запрос. Это называется предварительным запросом (preflight request). Если сервер не ответит правильными заголовками, браузер заблокирует основной запрос, даже не отправив его.
Почему это важно? Потому что многие разработчики сначала пробуют отправить запрос с пользовательскими заголовками или методами вроде PUT или DELETE, не понимая, что браузер сначала отправит OPTIONS. Если сервер не настроен отвечать на OPTIONS, запрос не пройдёт, даже если сервер готов обработать основной запрос.
Почему сервер должен отвечать на OPTIONS
Когда браузер отправляет предварительный запрос OPTIONS, сервер должен ответить заголовками, которые описывают, какие запросы он разрешает. Например:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Origin: https://example.com
Если сервер не отвечает на OPTIONS или отвечает неполными заголовками, браузер заблокирует основной запрос. При этом сервер может даже не знать, что запрос был заблокирован — браузер не отправляет основной запрос, если OPTIONS не прошёл.
Например, если скрипт на https://example.com пытается отправить PUT запрос на https://api.example.com, браузер сначала отправит OPTIONS. Если сервер не ответит заголовком Access-Control-Allow-Methods: PUT, браузер заблокирует основной запрос, даже если сервер готов его обработать.
Что менять на сервере, чтобы запросы проходили
1. Указать разрешенный источник
Самая частая ошибка — сервер не указывает Access-Control-Allow-Origin или указывает его неверно. Например, если скрипт на https://example.com пытается обратиться к https://api.example.com, сервер должен вернуть:
Access-Control-Allow-Origin: https://example.com
Если сервер должен разрешать доступ с нескольких доменов, нельзя просто перечислить их в заголовке через запятую. Вместо этого сервер должен проверять заголовок Origin из запроса и возвращать его же в Access-Control-Allow-Origin. Например, на Nginx это можно сделать так:
if ($http_origin ~* (https?://example\.com|https?://another\.com)) {
set $cors "true";
}
if ($cors = "true") {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
}
Если сервер возвращает Access-Control-Allow-Origin: *, это работает только для запросов без credentials. Если скрипт отправляет куки или токены, браузер требует явного указания домена.
2. Разрешить нужные методы и заголовки
Если скрипт использует нестандартные методы (например, PUT или DELETE), сервер должен явно разрешить их в заголовке Access-Control-Allow-Methods. То же касается заголовков: если скрипт добавляет пользовательские заголовки (например, Authorization или X-Custom-Header), сервер должен разрешить их в Access-Control-Allow-Headers.
Например, если скрипт отправляет запрос с заголовком Authorization, сервер должен вернуть:
Access-Control-Allow-Headers: Authorization
Если сервер не разрешит этот заголовок, браузер заблокирует запрос.
3. Обрабатывать предварительные запросы OPTIONS
Сервер должен корректно отвечать на предварительные запросы OPTIONS. В большинстве фреймворков это делается автоматически, но если сервер написан вручную, нужно добавить обработчик для OPTIONS. Например, на Express.js:
app.options('/api/data', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.send();
});
Если сервер не отвечает на OPTIONS, браузер заблокирует основной запрос, даже если сервер готов его обработать.
4. Разрешить credentials, если нужны куки или токены
Если скрипт отправляет куки или токены в заголовке Authorization, сервер должен разрешить это с помощью:
Access-Control-Allow-Credentials: true
При этом Access-Control-Allow-Origin не может быть *, он должен быть явным доменом. Например:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Если сервер вернёт Access-Control-Allow-Origin: * вместе с Access-Control-Allow-Credentials: true, браузер заблокирует запрос, потому что wildcard несовместим с credentials.
Почему не работает wildcard (*) и когда его использовать
Заголовок Access-Control-Allow-Origin: * разрешает доступ с любого домена, но только для запросов без credentials. Если скрипт отправляет куки или токены, браузер требует явного указания домена в Access-Control-Allow-Origin. Wildcard в этом случае не работает.
Когда можно использовать wildcard? Только если:
- Запрос не отправляет credentials (куки или токены).
- Сервер не требует аутентификации.
- Данные на сервере нечувствительные и доступны всем.
Например, если сервер отдаёт публичные данные, можно использовать wildcard:
Access-Control-Allow-Origin: *
Но если скрипт отправляет токен в заголовке Authorization, wildcard не сработает, и браузер заблокирует запрос.
Тупик: что пробуют первым и почему это не помогает
Чаще всего разработчики сначала пробуют добавить Access-Control-Allow-Origin: * и надеются, что этого достаточно. Но это не работает в большинстве случаев, потому что:
-
Запрос не простой. Например, если скрипт использует метод
PUTили добавляет пользовательский заголовокAuthorization, браузер отправит предварительный запросOPTIONS. Если сервер не отвечает наOPTIONSили не разрешает нужные методы и заголовки, запрос будет заблокирован. -
Скрипт отправляет credentials. Если скрипт отправляет куки или токены, wildcard
*не работает, и браузер требует явного указания домена. -
Сервер не разрешает нужные методы или заголовки. Даже если
Access-Control-Allow-Originуказан правильно, браузер заблокирует запрос, если сервер не разрешил нужные методы или заголовки.
Например, если скрипт отправляет запрос с методом DELETE, но сервер не разрешает его в Access-Control-Allow-Methods, браузер заблокирует запрос, даже если Access-Control-Allow-Origin указан правильно.
Как проверить, что сервер настроен правильно
Чтобы понять, почему запрос блокируется, можно использовать инструменты разработчика в браузере:
- Откройте вкладку Network в инструментах разработчика.
- Отправьте запрос и найдите его в списке.
- Посмотрите на предварительный запрос
OPTIONS(если он был отправлен) и на основной запрос. - Проверьте заголовки ответа сервера на наличие
Access-Control-Allow-Origin,Access-Control-Allow-MethodsиAccess-Control-Allow-Headers. - Сравните их с параметрами запроса. Если что-то не совпадает, браузер заблокирует запрос.
Например, если скрипт отправляет запрос с заголовком X-Custom-Header, но сервер не разрешает его в Access-Control-Allow-Headers, браузер покажет ошибку в консоли.
Альтернативы, если сервер не под вашим контролем
Если сервер не под вашим контролем и не поддерживает CORS, есть несколько обходных путей:
-
Обратный прокси. Можно настроить свой сервер как прокси, который будет добавлять нужные заголовки CORS. Например, если скрипт на
https://example.comпытается обратиться кhttps://api.another.com, можно настроить прокси наhttps://example.com/api, который будет проксировать запросы наhttps://api.another.comи добавлять заголовки CORS. -
JSONP. Это устаревший метод, который работает только для
GETзапросов. Сервер оборачивает данные в вызов функции JavaScript, и браузер выполняет этот код. Но JSONP небезопасен и не поддерживает современные методы вродеPOSTилиPUT. -
Отключение CORS в браузере. Это возможно только для разработки, например, с помощью расширений или флагов браузера. Но это небезопасно и не подходит для продакшена.
Лучший вариант — настроить сервер правильно, но если это невозможно, обратный прокси — самое надёжное решение.
Позиция редакции
CORS — это не баг, а необходимая защита, которая предотвращает кражу данных пользователя. Браузер блокирует запросы не потому, что ему так захотелось, а потому, что сервер не дал явного разрешения на доступ. Поэтому правильное решение — не обходить CORS, а настроить сервер так, чтобы он корректно отвечал на предварительные запросы и разрешал нужные методы и заголовки.
Это не всегда удобно, особенно если сервер не под вашим контролем. В таких случаях можно использовать обратный прокси, который будет добавлять нужные заголовки. Но это временное решение — лучше всё же настроить сервер правильно, чтобы избежать проблем с безопасностью и совместимостью.
Единственное условие, при котором позиция неверна: если вы работаете в закрытой сети, где все запросы идут на доверенные домены, и безопасность не критична. В этом случае можно отключить CORS в браузере или использовать расширения, но это небезопасно для продакшена и не должно использоваться в публичных приложениях.