Если что-то не работает
Ошибка CORS после публикации: фронтенд не видит бэкенд
Коротко
Ошибка CORS означает, что браузер заблокировал ответ бэкенда, потому что сервер не разрешил запросы с адреса вашего фронтенда. Чинится это на бэкенде: в его настройках CORS нужно перечислить точный адрес фронтенда с https и без косой черты в конце. Локально ошибки не было потому, что dev-сервер Vite или Create React App проксировал запросы, и браузер считал фронтенд и бэкенд одним сайтом. Заодно проверьте, что адрес API в коде фронтенда указан с https и задан до сборки, а не после.
- CORS — правило браузера, поэтому тот же запрос из curl, Postman или с сервера проходит без ошибки.
- Адреса считаются разными, если отличается хотя бы протокол, домен или порт: http://localhost:5173 и https://app.example.com — это два разных источника.
- Разрешение «*» в заголовке Access-Control-Allow-Origin браузер не принимает, если запрос идёт с cookies или авторизацией через credentials.
- Переменные VITE_* и NEXT_PUBLIC_* вшиваются в код фронтенда при сборке, поэтому после их смены проект нужно собрать и опубликовать заново.
- Страница, открытая по https, не может обращаться к API по http: браузер блокирует такой запрос как смешанное содержимое.
Локально всё работало: фронтенд открывался, данные с бэкенда приходили. После публикации страница пустая, кнопки ничего не делают, а в консоли браузера красная строка вроде «has been blocked by CORS policy: No Access-Control-Allow-Origin header is present». При этом если открыть адрес API прямо в браузере или дёрнуть его через curl, ответ приходит нормальный. Это не поломка хостинга и не ошибка в логике кода — так браузер защищает пользователя.
CORS (дословно «обмен ресурсами между разными источниками») — правило, по которому браузер отдаёт странице ответ от чужого сайта только с разрешения этого сайта. Ваш фронтенд и бэкенд после публикации живут по разным адресам, значит, для браузера это два разных сайта, и бэкенд должен явно сказать: «запросы с такого-то адреса можно». Ниже — почему локально этого не требовалось и как настроить всё правильно.
| Фреймворк | Что подключить | Что указать |
|---|---|---|
| FastAPI | встроенный CORSMiddleware | allow_origins со списком адресов фронтенда; allow_credentials=True, если нужны cookies |
| Flask | пакет Flask-CORS | CORS(app, origins=[адрес фронтенда]); supports_credentials=True для cookies |
| Django | пакет django-cors-headers | CORS_ALLOWED_ORIGINS со списком адресов; CorsMiddleware в MIDDLEWARE как можно выше |
| Express | пакет cors | app.use(cors({ origin: адрес фронтенда, credentials: true })) до объявления маршрутов |
| NestJS | встроенный app.enableCors | origin с адресом фронтенда и credentials: true для cookies |
| Spring Boot | аннотация @CrossOrigin или addCorsMappings | allowedOrigins с адресом фронтенда; allowCredentials(true) для cookies |
Поймите, почему локально ошибки не было#
Во время разработки Vite и Create React App обычно проксируют запросы: в настройке server.proxy у Vite или в поле proxy в package.json у CRA указано, что запросы к /api нужно пересылать на бэкенд. Браузер при этом видит один адрес, localhost:5173, и CORS не включается вовсе. После сборки прокси исчезает: остаются готовые файлы фронтенда, которые обращаются к бэкенду напрямую по другому адресу. Поэтому ошибка появляется именно после публикации, хотя код не менялся.
Разрешите адрес фронтенда на бэкенде#
Подключите в бэкенде поддержку CORS и перечислите в ней точный адрес, где открывается фронтенд: с https, с доменом, без косой черты и пути в конце. Для FastAPI это встроенный CORSMiddleware, для Flask пакет Flask-CORS, для Django пакет django-cors-headers, для Express пакет cors — точные настройки собраны в таблице. Если у вас и локальный, и опубликованный фронтенд, перечислите оба адреса. Удобно хранить список в переменной окружения, чтобы менять его без правки кода.
Не ставьте звёздочку, если используете cookies#
Разрешение «*» значит «любой сайт», и для открытого API без входа это допустимо. Но если фронтенд отправляет cookies или запрос с credentials, браузер отвергнет ответ со звёздочкой, и ошибка CORS останется, только с другим текстом. В этом случае укажите конкретный адрес фронтенда и включите разрешение на credentials в настройках CORS. Это ещё и безопаснее: чужая страница не сможет делать запросы от имени ваших пользователей.
Задайте адрес API до сборки фронтенда#
Фронтенд узнаёт адрес бэкенда из переменной: у Vite она должна начинаться с VITE_, у Next.js с NEXT_PUBLIC_, у Create React App с REACT_APP_. Такие переменные подставляются в код в момент сборки, а не при открытии страницы, поэтому смена значения после сборки ничего не даст. Надёжный способ — положить в папку фронтенда файл .env.production со строкой вроде VITE_API_URL и адресом бэкенда: этот адрес не секрет, он всё равно виден любому посетителю в коде страницы. После правки опубликуйте фронтенд заново.
Проверьте, что API вызывается по https#
Если страница открыта по https, а адрес API в коде начинается с http, браузер заблокирует запрос как смешанное содержимое, и это легко принять за ту же ошибку CORS. Перепишите адрес на https. Отдельно проверьте, не остался ли в коде http://localhost:8000 или похожий локальный адрес: опубликованная страница будет стучаться на компьютер посетителя, а не на ваш сервер.
Убедитесь, что бэкенд отвечает на предварительный запрос#
Перед запросом с JSON или заголовком Authorization браузер сначала отправляет короткий предварительный запрос методом OPTIONS и ждёт в ответ заголовки CORS. Подключённая библиотека CORS отвечает на него сама, но если бэкенд защищает все маршруты проверкой входа, она может отбить и этот запрос, и тогда основной не уйдёт вовсе. В консоли браузера на вкладке «Сеть» видно, на каком из двух запросов всё остановилось. Если упал OPTIONS, пропустите его мимо проверки авторизации.
Ошибка CORS почти всегда закрывается тремя действиями: точный адрес фронтенда в настройках бэкенда, адрес API с https в переменной сборки и повторная публикация фронтенда. Есть и способ обойтись без CORS вовсе: отдавать собранный фронтенд тем же приложением, что и API, тогда адрес у них один. В Netrun каждый проект получает свою ссылку с HTTPS, поэтому фронтенд и бэкенд, опубликованные отдельными проектами, — это два адреса, и настройка CORS нужна; логи бэкенда при этом видны в кабинете в реальном времени. Учтите, что на бесплатном тарифе доступен один проект. Попробовать Netrun.
Частые вопросы
Почему через Postman запрос работает, а из браузера нет?
Потому что CORS проверяет только браузер. Postman, curl и серверный код не смотрят на заголовки Access-Control-Allow-Origin и показывают ответ как есть. Браузер же защищает пользователя от чужих страниц и без разрешения бэкенда не отдаёт ответ скрипту на странице.
Можно ли починить CORS на стороне фронтенда?
Нет, разрешение выдаёт только сервер, к которому идёт запрос. Режим no-cors в fetch ошибку не убирает, а просто прячет содержимое ответа, поэтому данные всё равно не прочитать. Нужно либо настроить CORS на бэкенде, либо отдавать фронтенд и API с одного адреса.
Что делать, если бэкенд чужой и настроить его нельзя?
Тогда запросы к нему нужно делать не из браузера, а со своего сервера: ваш бэкенд обращается к чужому API и пересылает ответ фронтенду. Серверные запросы правилу CORS не подчиняются. Заодно ключи доступа к чужому API останутся на сервере и не попадут в код страницы.
Безопасно ли поставить allow_origins равным звёздочке?
Для открытого API без входа и без cookies это допустимо: любой сайт сможет читать ответы, но они и так публичные. Если есть вход, cookies или личные данные, указывайте конкретные адреса фронтенда. Со звёздочкой и credentials браузер всё равно откажет.
Почему ошибка CORS появляется только иногда?
Частая причина — сам бэкенд падает с ошибкой 500 или 502, а такой ответ приходит без заголовков CORS, и браузер пишет про CORS, хотя дело в другом. Посмотрите логи бэкенда в момент ошибки. Если там исключение, чинить нужно его, а настройки CORS в порядке.
А React, Vue, Next.js, Nuxt, Svelte или Astro?
Да, все они работают. Загрузите исходный код с package.json — собирать проект у себя и класть готовую сборку в архив не нужно, сборка идёт у нас. Если фреймворк умеет и в готовые страницы, и в серверный режим, подойдут оба варианта: в серверном приложение должно слушать порт из переменной окружения. Одна важная деталь: значения, которые попадают в собранный фронтенд, видны любому посетителю — настоящие ключи держите на стороне сервера.
Где указать токены и другие секретные значения?
В проекте есть вкладка «Секреты» — там задаются значения из кода, например токен от BotFather. Мы храним их в зашифрованном виде: названия переменных вы видите, сами значения не показываем никому, включая вас.
Где посмотреть логи и статус проекта?
На странице проекта есть статус, логи и история событий — по ним видно, что происходит с проектом прямо сейчас. Лента логов показывает и более ранние строки: пролистайте её вверх, и они подгрузятся сами.