JavaScript Fetch: межсайтовые запросы (CORS)
Межсайтовые запросы с Fetch API: как работает CORS, опции mode и credentials, preflight-запросы и лучшие практики обработки ошибок.
Межсайтовые запросы позволяют веб-странице загружать данные с сервера, расположенного на другом источнике, чем сама страница. Они лежат в основе почти каждого современного приложения: обращение к публичному API, взаимодействие с собственным бэкендом на другом поддомене или встраивание стороннего сервиса. На этой странице объясняется, как работают правила CORS в браузере, как выполнять межсайтовые запросы с помощью Fetch API, а также как правильно работать с учётными данными, preflight-запросами и ошибками.
Что считается «межсайтовым»
Источник (origin) — это комбинация протокола + хоста + порта. Два URL имеют один и тот же источник только тогда, когда все три компонента совпадают. Если хотя бы один из них отличается, запрос между ними является межсайтовым.
Запрос с https://app.example.com на… | Один источник? | Почему |
|---|---|---|
https://app.example.com/api/users | Да | одинаковые протокол, хост, порт |
http://app.example.com/api | Нет | разные протоколы (http vs https) |
https://api.example.com/users | Нет | разные хосты (поддомен считается другим) |
https://app.example.com:8443/api | Нет | разные порты |
Запросы в пределах одного источника не имеют ограничений. Межсайтовые запросы регулируются правилами CORS.
Что такое CORS (и чем он не является)
Cross-Origin Resource Sharing (CORS) — это механизм безопасности браузера, который определяет, разрешено ли JavaScript на одном источнике читать ответ с другого источника. Решение принимает сервер, который явно разрешает это, отправляя специальные HTTP-заголовки ответа. Браузер затем применяет эти правила.
Две вещи легко перепутать:
- CORS — это не то, что можно исправить только во фронтенд-коде. Если сервер не отправляет нужные заголовки, никакая опция
fetchне позволит успешно прочитать ответ. - Запрос зачастую всё равно доходит до сервера и выполняется там; CORS лишь запрещает скрипту читать ответ. Именно поэтому CORS не является заменой аутентификации.
Межсайтовый запрос с помощью Fetch
Fetch API — это современный способ выполнения HTTP-запросов на основе промисов. Обычный вызов fetch к другому источнику уже является межсайтовым — браузер обрабатывает CORS автоматически.
Это работает, потому что jsonplaceholder.typicode.com возвращает Access-Control-Allow-Origin: *. Если бы этого не было, браузер заблокировал бы чтение ответа, и fetch вернул бы ошибку. Подробнее о модели запрос/ответ читайте в главе Fetch API.
Заголовки сервера, которые это обеспечивают
При межсайтовом запросе сервер должен включить заголовки CORS, разрешающие его. Наиболее часто встречающиеся:
Access-Control-Allow-Origin— какой источник (или источники) может читать ответ (конкретный источник или*для любого).Access-Control-Allow-Methods— какие HTTP-методы разрешены (используется в ответах на preflight).Access-Control-Allow-Headers— какие заголовки запроса может отправлять клиент (используется в ответах на preflight).Access-Control-Allow-Credentials— разрешена ли отправка cookies и данных авторизации (должно бытьtrueдля разрешения учётных данных).
Эти параметры настраиваются на сервере, а не в fetch. Фронтенд управляет только запросом.
Опция mode
Опция mode в Fetch определяет поведение при межсайтовых запросах:
'cors'— значение по умолчанию. Запрос разрешается только если сервер возвращает соответствующие заголовки CORS; в противном случае чтение блокируется.'same-origin'— немедленно завершается ошибкой для любого межсайтового URL.'no-cors'— отправляет запрос, но возвращает непрозрачный ответ: вы не можете прочитать его статус, заголовки или тело. Полезно только для сценариев «отправил и забыл», например при кешировании изображения в сервис-воркере.
Поскольку 'cors' и так является значением по умолчанию, явно указывать его почти никогда не нужно, однако это делает намерение кода очевидным:
На сервере избегайте Access-Control-Allow-Origin: * в рабочей среде. Разрешайте только те источники, которым вы доверяете.
Отправка учётных данных
По умолчанию межсайтовые запросы fetch не отправляют cookies или заголовки HTTP-аутентификации. Чтобы включить их, установите опцию credentials в 'include'. Сервер также должен ответить с Access-Control-Allow-Credentials: true и указать ваш конкретный источник в Access-Control-Allow-Origin — значение * отклоняется при использовании учётных данных. Подробнее о том, как cookies работают между сайтами, читайте в главе Cookies и document.cookie.
async function fetchWithCredentials(url) {
try {
const response = await fetch(url, {
mode: 'cors',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error);
}
}
fetchWithCredentials('https://api.crossorigin.com/secure-data');Когда политика CORS блокирует межсайтовое чтение, браузер не передаёт скрипту ответ. Вместо этого fetch отклоняется с TypeError («Failed to fetch»), и ошибка попадает в блок catch — но не в проверку if (!response.ok).
Preflight-запросы
Для запросов, которые браузер считает потенциально небезопасными, он сначала автоматически отправляет запрос OPTIONS — preflight — чтобы спросить у сервера, разрешён ли реальный запрос. Ваш код никогда не отправляет этот вызов OPTIONS самостоятельно — это делает браузер.
Запрос вызывает preflight, если он не является «простым запросом», то есть когда он:
- использует метод, отличный от
GET,HEADилиPOST; - включает пользовательские заголовки запроса (например
AuthorizationилиX-Api-Key); - использует
Content-Type, отличный отapplication/x-www-form-urlencoded,multipart/form-dataилиtext/plain(отправка JSON сapplication/json— наиболее распространённый случай).
Типичный JSON-запрос POST поэтому требует двух обращений к серверу — preflight, а затем реальный запрос:
async function createPost(url, payload) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // triggers a preflight
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
}
}
createPost('https://api.example.com/posts', { title: 'Hello' });Чтобы preflight прошёл успешно, сервер должен ответить на запрос OPTIONS, включив Access-Control-Allow-Methods и Access-Control-Allow-Headers, охватывающие то, что будет использоваться в реальном запросе. Если проверка не пройдена, браузер генерирует сетевую ошибку, и реальный запрос так и не отправляется.
Различие типов ошибок
Ошибки CORS печально известны своей запутанностью, поскольку браузер скрывает подробности в целях безопасности. Используйте следующий чек-лист:
fetchотклоняется («Failed to fetch») — как правило, это блокировка CORS, сетевой сбой или неудавшийся preflight. Проверьте консоль браузера для получения конкретного сообщения об ошибке CORS — оно недоступно в JavaScript.response.okравноfalse— запрос выполнен успешно и CORS пройден, но сервер вернул статус 4xx/5xx. Это ошибка приложения, а не ошибка CORS.- Cookies не отправляются — вы забыли указать
credentials: 'include', или сервер не возвращаетAccess-Control-Allow-Credentials: trueс конкретным источником.
Оборачивайте вызовы в try/catch и отдельно проверяйте response.ok, как это сделано в приведённых примерах.
Лучшие практики
- Ограничьте
Access-Control-Allow-Origin. Перечисляйте конкретные источники, которым вы доверяете, вместо*— особенно если используются учётные данные (*отклоняется при их наличии). - Всегда используйте HTTPS. Это защищает данные при передаче и требуется многими API безопасного контекста.
- Сокращайте количество preflight-запросов. Избегайте лишних пользовательских заголовков и настройте на сервере
Access-Control-Max-Age, чтобы браузер кешировал результаты preflight. - Обрабатывайте ошибки корректно. Разделяйте транспортные/CORS-ошибки (
catch) и ошибки HTTP-статуса (!response.ok) и предоставляйте пользователю понятные сообщения. - Не полагайтесь на CORS как на средство безопасности. Он контролирует лишь то, что браузеры позволяют скриптам читать; он не аутентифицирует вызывающую сторону. Проверяйте и авторизуйте каждый запрос на стороне сервера.
Связанные главы
- Fetch API — основа каждого запроса на этой странице.
- Отмена fetch — отмена медленных или ненужных запросов.
- Работа с JSON — разбор и сериализация тел запросов и ответов.
- Cookies: document.cookie — как cookies взаимодействуют с межсайтовыми учётными данными.
Итог
Запрос является межсайтовым, если его протокол, хост или порт отличается от параметров страницы. CORS позволяет серверу решать, какие источники могут читать ответ, а браузер применяет это решение. В Fetch режим 'cors' используется по умолчанию; опция credentials позволяет отправлять cookies, для нестандартных вызовов ожидайте preflight-запрос OPTIONS, а ошибки CORS обрабатывайте в catch, проверяя response.ok для ошибок на уровне HTTP.