// Engineering Log
HTTPS и веб-сервер на практике: Часть 3 — Что такое CORS: ошибки в консоли браузера и настройка в Nginx
Опубликовано 02.11.2026 · Автор: Михаил Горбачёв
// Быстрый маршрут
Эта статья относится к теме Серверы и инфраструктура.
CORS (Cross-Origin Resource Sharing) — механизм, с помощью которого сервер разрешает JavaScript-коду с других сайтов читать свои ответы. По умолчанию браузер этого не позволяет: скрипт на https://app.example.ru может отправить запрос на https://api.example.ru, но прочитать ответ не сможет, если сервер API явно не разрешил. Отсюда ошибки вида «blocked by CORS policy» в консоли, которые появляются, как только фронтенд и API оказываются на разных адресах. Ниже — как устроена проверка, что означает каждая типичная ошибка и как настроить сервер. Сами заголовки HTTP кратко разобраны в статье «Заголовки HTTP: какие бывают и за что отвечают».
Что такое «источник»
Источник (origin) — это схема, хост и порт вместе. Два адреса относятся к одному источнику, только если все три части совпадают:
| Адрес страницы | Адрес запроса | Тот же источник? |
|---|---|---|
https://example.ru | https://example.ru/api | да |
https://example.ru | https://api.example.ru | нет — другой хост |
https://example.ru | http://example.ru | нет — другая схема |
http://localhost:3000 | http://localhost:8000 | нет — другой порт |
Поддомен — это уже другой источник. Локальная разработка с фронтендом на порту 3000 и API на 8000 — тоже.
Что именно блокирует браузер
Правило одного источника (same-origin policy) запрещает скрипту читать ответы с чужих источников. Отправлять запросы оно в большинстве случаев не запрещает. CORS — способ ослабить это правило для конкретных источников.
Отсюда три важных следствия:
- CORS защищает пользователя, а не сервер. curl, Postman, скрипт на Python, другой сервер никакого CORS не проверяют и получают ответ всегда. Ограничить доступ к API можно только аутентификацией;
- «простой» запрос с чужого сайта доходит до сервера и выполняется — браузер лишь не отдаёт ответ скрипту. От подделки таких запросов защищает не CORS, а CSRF-токены и
SameSiteу cookie («Атаки на уровне HTTP»); - ошибка CORS — это ответ сервера без нужных заголовков, а не сетевой сбой. Сервер при этом мог вернуть и 200, и 500: на вкладке Network виден настоящий код.
Простые запросы и предварительный запрос
Простой запрос браузер отправляет сразу и проверяет заголовки ответа. Запрос считается простым, если одновременно:
- метод — GET, HEAD или POST;
- заголовки — только из короткого разрешённого списка (
Accept,Accept-Language,Content-Language,Content-Typeи некоторые другие), безAuthorizationи собственныхX-…; Content-Type— толькоapplication/x-www-form-urlencoded,multipart/form-dataилиtext/plain.
Любой другой запрос — PUT, PATCH, DELETE, JSON с Content-Type: application/json, запрос с Authorization — требует предварительного запроса (preflight). Браузер сначала отправляет OPTIONS:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.ru
Origin: https://app.example.ru
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-typeСервер должен ответить кодом 2xx и разрешениями:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.ru
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 7200
Vary: OriginТолько после этого уходит настоящий PUT, и его ответ тоже должен содержать Access-Control-Allow-Origin. Access-Control-Max-Age позволяет браузеру кешировать результат предварительного запроса; Chrome ограничивает срок двумя часами.
Заголовки ответа
Access-Control-Allow-Origin— какой источник может читать ответ: конкретный (https://app.example.ru) или любой (*). Значение одно: список через запятую не допускается;Access-Control-Allow-Methods,Access-Control-Allow-Headers— разрешённые методы и заголовки, только в ответе наOPTIONS;Access-Control-Allow-Credentials: true— разрешить запросы с cookie иAuthorization;Access-Control-Expose-Headers— какие заголовки ответа доступны скрипту. По умолчанию скрипт видит только несколько базовых (Content-Type,Content-Length,Cache-Controlи т. п.); чтобы прочитатьX-Total-CountилиContent-Disposition, их нужно перечислить;Access-Control-Max-Age— кеширование предварительного запроса, в секундах;Vary: Origin— обязательно, еслиAllow-Originзависит отOriginзапроса. Иначе CDN или браузер закеширует ответ с разрешением для одного сайта и отдаст его другому.
Запросы с cookie и авторизацией
По умолчанию fetch к чужому источнику не отправляет cookie. Чтобы отправлял:
fetch("https://api.example.ru/v1/me", { credentials: "include" });Тогда сервер должен:
- вернуть конкретный источник в
Access-Control-Allow-Origin—*вместе с учётными данными запрещён; - вернуть
Access-Control-Allow-Credentials: true; - у cookie должен быть подходящий
SameSite: для другого сайта —SameSite=None; Secure. Для поддоменов одного сайта (app.example.ruиapi.example.ru— один «сайт» с точки зренияSameSite) хватаетLax.
Токен в заголовке Authorization проще cookie: не нужен credentials: "include", но нужен Authorization в Access-Control-Allow-Headers.
Ошибки в консоли и что они значат
No 'Access-Control-Allow-Origin' header is present on the requested resource
Сервер не вернул заголовок. Причины: CORS не настроен; настроен, но ответ пришёл с ошибкой (404, 500, 502), а заголовки добавляются только в успешные ответы — в Nginx не хватает always; запрос ушёл не на тот адрес. Посмотрите на вкладке Network настоящий код ответа.
Response to preflight request doesn't pass access control check: It does not have HTTP ok status
Ответ на OPTIONS не 2xx. Чаще всего OPTIONS требует авторизации (401), не поддерживается фреймворком (404, 405) или перенаправляется. Предварительный запрос всегда идёт без cookie и Authorization — сервер должен отвечать на него без проверки доступа.
Redirect is not allowed for a preflight request
На OPTIONS пришёл редирект: с http на https, со слеша на без слеша (/v1/orders → /v1/orders/), на страницу входа. Исправьте адрес во фронтенде, чтобы редиректа не было.
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'
Запрос с cookie, а в ответе *. Нужен конкретный источник и Allow-Credentials: true.
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response
Заголовок не перечислен в Access-Control-Allow-Headers. То же для content-type при отправке JSON и для любых собственных заголовков.
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response
Метод не перечислен в Access-Control-Allow-Methods.
The 'Access-Control-Allow-Origin' header contains multiple values '…, …', but only one is allowed
Заголовок добавляют два слоя сразу — например, и Nginx, и приложение, или и приложение, и CDN. Оставьте CORS в одном месте.
Ответ приходит, но response.headers.get("X-Total-Count") возвращает null
Заголовок не перечислен в Access-Control-Expose-Headers.
Настройка в Nginx
Разрешённые источники — по списку через map, чтобы не отражать любой Origin:
map $http_origin $cors_origin {
default "";
"https://app.example.ru" $http_origin;
"https://admin.example.ru" $http_origin;
"~^http://localhost:\d+$" $http_origin; # только для тестового стенда
}
server {
listen 443 ssl;
server_name api.example.ru;
location / {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Request-ID" always;
add_header Access-Control-Max-Age 7200 always;
add_header Vary Origin always;
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Expose-Headers "X-Total-Count, Content-Disposition" always;
add_header Vary Origin always;
proxy_pass http://127.0.0.1:8080;
}
}Пояснения:
ifсreturnвнутриlocation— одно из допустимых примененийifв Nginx;- если источника нет в списке,
$cors_originпустой, и Nginx не выводит заголовок с пустым значением — браузер заблокирует чтение ответа; alwaysнужен, чтобы заголовки были и в ответах 4xx/5xx — иначе вместо понятной ошибки API фронтенд увидит ошибку CORS;- если в
locationпоявится ещё одинadd_header, заголовки с уровняserverв нём перестанут действовать — держите все заголовки в одном месте.
Настройка в приложении
Если CORS настраивается в приложении, Nginx заголовки не добавляет вовсе:
- Express: пакет
cors—app.use(cors({ origin: ["https://app.example.ru"], credentials: true })); - Django:
django-cors-headers—CORS_ALLOWED_ORIGINS = ["https://app.example.ru"],CORS_ALLOW_CREDENTIALS = True; - FastAPI:
CORSMiddlewareсallow_origins,allow_credentials,allow_methods,allow_headers; - Spring:
@CrossOriginили глобальныйCorsConfigurationSource.
Промежуточный слой CORS в приложении должен стоять раньше аутентификации, иначе OPTIONS получит 401.
Как обойтись без CORS
Самое простое решение — не делать запросы кросс-доменными:
- один домен для фронтенда и API:
https://example.ru/— сайт,https://example.ru/api/— API через тот же Nginx. Источник один, CORS не нужен, cookie работают безSameSite=None; - прокси в разработке: в Vite —
server.proxy, в Create React App — полеproxyвpackage.json. Фронтенд обращается к/apiна своём порту, а сервер разработки перенаправляет запросы на API.
Как проверить
Предварительный запрос можно воспроизвести curl’ом:
curl -si -X OPTIONS https://api.example.ru/v1/orders \
-H 'Origin: https://app.example.ru' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: authorization, content-type' \
| grep -i '^access-control\|^HTTP\|^vary'И основной — с тем же Origin:
curl -si https://api.example.ru/v1/orders -H 'Origin: https://app.example.ru' | grep -i '^access-control'С источником не из списка заголовка Access-Control-Allow-Origin быть не должно.
Типичные ошибки
Access-Control-Allow-Originберётся изOriginзапроса без проверки по списку — это разрешение для любого сайта, причём вместе с cookie.Access-Control-Allow-Origin: *на API с авторизацией по cookie — не работает и говорит о том, что CORS настраивали наугад.- Нет
Vary: Originпри ответе, зависящем отOrigin, — CDN раздаёт ответ с чужим разрешением. - CORS настроен и в Nginx, и в приложении — дублирующийся заголовок.
OPTIONSзакрыт аутентификацией или ограничением частоты — весь фронтенд ломается при любой нагрузке.- Попытка «починить CORS» на фронтенде (
mode: "no-cors") — запрос уходит, но ответ становится полностью недоступен скрипту.
// Похожая задача
Если у вас похожая ситуация
Эта статья относится к одной из рабочих тем. Можно продолжить чтение по теме, перейти на главную, чтобы понять, чем я занимаюсь, или сразу открыть услуги.
Тема статьи
Серверы и инфраструктура
VPS, Linux, веб-стек, миграции, хостинг, базы данных и базовая эксплуатация.
Часто с этим приходят
- Перенести сайт или сервис на новый сервер
- Настроить Linux, Nginx, базу данных и бэкапы
- Разобраться, почему всё работает нестабильно
// Следующий шаг
Если вам нужна не только статья, а помощь по этой теме, удобнее сразу перейти в услугу. Главная и подборка материалов остаются рядом.
Открыть услуги// Reviews
Отзывы по теме
Спасибо Михаилу за отзывчивость. Созвонились, объяснил как сделать самому. Обращаюсь уже второй раз, все супер и оперативно.
Спасибо Михаилу за отзывчивость. Созвонились, объяснил как сделать самому. Обращаюсь уже второй раз, все супер и оперативно.
Хочу выразить огромную благодарность специалисту, который настроил мне ЧПУ на OpenCart. Настроить ЧПУ оказалось легко и просто, и я рад, что наконец нашел профессионала, который сделал всё качественно и без лишних сложностей. До этого я сменил четырёх специалистов, и каждый раз возникали проблемы с настройкой, но этот человек справился с задачей идеально.
Хочу выразить огромную благодарность специалисту, который настроил мне ЧПУ на OpenCart. Настроить ЧПУ оказалось легко и просто, и я рад, что наконец нашел профессионала, который сделал всё качественно и без лишних …
Отличная работа, обращаюсь не первый раз, находит решения к сложным задачам. Рекомендую.
Отличная работа, обращаюсь не первый раз, находит решения к сложным задачам. Рекомендую.
Отличная работа! Выполнил поставленную задачу в срок и без ошибок. Было приятно сотрудничать, рекомендую.
Отличная работа! Выполнил поставленную задачу в срок и без ошибок. Было приятно сотрудничать, рекомендую.
Отличный специалист, вник в проблему, разобрался, исправил. Рекомендую.
Отличный специалист, вник в проблему, разобрался, исправил. Рекомендую.
Нужно было решить проблему с SSL сертификатом на сервере, который был выпущен через Ngnix Proxy manager. Михаил уточнил все детали, как у меня все устроено, попросил доступы чтобы оценить реальность решения задачи, т. к. до этого не сталкивался с подобным сервисом. Быстро разобрался и решил мою проблему. Идеальное сотрудничество)
Нужно было решить проблему с SSL сертификатом на сервере, который был выпущен через Ngnix Proxy manager. Михаил уточнил все детали, как у меня все устроено, попросил доступы чтобы оценить реальность решения задачи, т. …
Диагностика Nginx Proxy Manager в docker контейнере и решение проблемы
15.03.2024 · ★ 5/5
// Contact
Нужна помощь?
Свяжись со мной и я помогу решить проблему
Написать в TelegramОтвечаю в течение рабочего дня (03:00–13:00 GMT)
Или оставьте заявку здесь:
// Related