Введение: когда браузер говорит "нет", а Postman — "да"
Представьте ситуацию: вы пишете фронтенд на JavaScript, делаете fetch запрос к API. В ответ — красная ошибка в консоли:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Вы хватаетесь за голову, открываете Postman, вставляете тот же URL, нажимаете Send — и о чудо, данные приходят! Сервер работает? Работает. Почему же браузер блокирует запрос?
Эта ситуация — классика веб-разработки. На Stack Overflow этот вопрос собрал тысячи просмотров. Сегодня мы заглянем под капот и разберёмся, почему так происходит, как работает CORS и как это исправить. Поехали!
Что такое CORS и зачем он нужен?
CORS (Cross-Origin Resource Sharing) — это механизм безопасности браузера. Он запрещает JavaScript с одного сайта (например, http://localhost:3000) делать запросы к другому (например, https://api.example.com), если сервер явно не разрешил это.
Зачем? Чтобы злоумышленник не мог через ваш браузер украсть данные с другого сайта. Без CORS любой скрипт мог бы читать вашу почту, банковские счета — ужас.
Но тут есть нюанс: CORS работает только в браузере. Postman, curl, серверные запросы (Node.js, Python) — они не проверяют CORS. Это первая и главная причина, почему Postman работает, а JavaScript — нет.
Как работает CORS под капотом: простыми словами
Когда ваш JavaScript (например, fetch) делает кросс-доменный запрос, браузер делает две вещи:
- Проверяет тип запроса.
- Если нужно — отправляет preflight-запрос (OPTIONS).
Простой запрос vs Preflight
Есть два типа кросс-доменных запросов:
- Простой запрос — если вы используете GET, POST или HEAD, и заголовки только базовые (Content-Type: text/plain, application/x-www-form-urlencoded, multipart/form-data). Браузер отправляет запрос и смотрит на ответный заголовок
Access-Control-Allow-Origin. - Preflight-запрос — если вы используете нестандартные заголовки (например,
Authorization), методы PUT/DELETE, или Content-Type: application/json. Браузер сначала отправляет OPTIONS-запрос, чтобы спросить: "Можно ли делать такой запрос?"
Вот пример preflight в коде:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Это вызывает preflight
'Authorization': 'Bearer my-token'
},
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error('CORS error:', err));
Если сервер не отвечает на OPTIONS корректным заголовком Access-Control-Allow-Origin — браузер блокирует основной запрос.
Почему Postman игнорирует CORS?
Postman — это не браузер. Он не использует политику одного источника (Same-Origin Policy). Он просто отправляет HTTP-запрос и получает ответ. Ему всё равно, откуда пришёл запрос — он не проверяет заголовки CORS. Сервер может вернуть что угодно, Postman покажет ответ.
Поэтому, когда вы тестируете API через Postman, вы видите, что сервер работает. Но браузер требует, чтобы сервер явно разрешил доступ с вашего домена.
Как исправить ошибку CORS: 3 реальных способа
1. Настроить сервер (правильный путь)
Если у вас есть доступ к серверу, добавьте заголовок Access-Control-Allow-Origin. Например, на Node.js с Express:
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); // Разрешаем всем
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(200); // Отвечаем на preflight
}
next();
});
app.get('/data', (req, res) => {
res.json({ message: 'Hello from server!' });
});
app.listen(3000);
Здесь * разрешает запросы с любого домена. В продакшене лучше указывать конкретный домен.
2. Использовать прокси (для разработки)
Если вы не контролируете сервер, можно настроить прокси на своём фронтенде. Например, в Webpack Dev Server:
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true
}
}
}
};
Тогда ваш fetch('/api/data') пойдёт через localhost, и CORS не понадобится.
3. Использовать CORS-расширение для браузера (только для тестов)
Есть расширения, которые отключают CORS в браузере (например, "CORS Unblock" для Chrome). Но это опасно и не для продакшена. Используйте только для локальной отладки.
Как отлаживать CORS-ошибки: чек-лист
Когда вы видите ошибку CORS, сделайте следующее:
- Откройте вкладку Network в DevTools браузера (F12).
- Посмотрите, есть ли запрос OPTIONS (preflight). Если есть — проверьте его ответ.
- Убедитесь, что сервер возвращает
Access-Control-Allow-Originс вашим доменом или*. - Проверьте, что сервер отвечает на OPTIONS статусом 200.
- Если используете
fetchсcredentials: 'include', тоAccess-Control-Allow-Originне может быть*— нужен конкретный домен.
Пример: как воспроизвести и починить
Допустим, вы пишете такой код:
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data));
Это сработает, потому что JSONPlaceholder разрешает CORS. А вот если бы сервер не разрешал — вы бы получили ошибку.
Попробуйте сделать запрос к своему локальному серверу без CORS:
// Сервер на Node.js без CORS
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'No CORS' }));
}).listen(4000);
// Фронтенд с localhost:3000
fetch('http://