Web & API

CORS

Cross-Origin Resource Sharing — механизм, которым сервер разрешает браузеру отдавать ответ коду со стороннего домена.

Что такое CORS

CORS расшифровывается как Cross-Origin Resource Sharing — совместное использование ресурсов между источниками. Это набор HTTP-заголовков, которыми сервер сообщает браузеру: коду с такого-то чужого домена можно читать мои ответы.

Механизм существует из-за same-origin policy — базового правила безопасности браузера: скрипт со страницы одного источника не может читать ответы от другого. Источник (origin) — это тройка «схема + хост + порт», и совпадать должны все три:

Со страницы https://app.example.com Тот же источник?
https://app.example.com/api/orders да
https://api.example.com/orders нет — другой хост
http://app.example.com/orders нет — другая схема
https://app.example.com:8443/orders нет — другой порт

Без same-origin policy открытая вкладка со зловредным сайтом могла бы отправить запрос в вашу почту — вместе с вашими куками — и прочитать ответ. CORS не «защита от хакеров», а способ аккуратно ослабить это правило там, где вы сами этого хотите.

Ключевой момент, экономящий часы отладки: CORS живёт только в браузере. Запрос из curl, Python-скрипта или с сервера на сервер CORS-проверок не проходит. Если curl работает, а браузер ругается — дело именно в CORS, а не в сервере.

Как это выглядит

Браузер добавляет к запросу заголовок Origin, сервер отвечает разрешением:

GET /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com

Нет заголовка в ответе — браузер получит данные, но не отдаст их коду и напишет в консоль ошибку. Именно поэтому во вкладке Network запрос выглядит успешным (200), а fetch при этом падает — HAR-файл хорошо показывает эту картину.

Preflight-запрос

Для «непростых» запросов браузер сначала спрашивает разрешение отдельным запросом методом OPTIONS — это preflight. Он срабатывает, если метод не GET/HEAD/POST, если Content-Type: application/json, или если есть кастомные заголовки вроде Authorization или X-API-Key. То есть практически любой вызов REST API вызывает preflight.

OPTIONS /v1/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Access-Control-Max-Age позволяет браузеру закешировать разрешение и не слать OPTIONS перед каждым вызовом — без него число запросов к API удваивается.

Настройка в Nginx

location /v1/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "https://app.example.com";
        add_header Access-Control-Allow-Methods "GET, POST, PATCH, DELETE";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
        add_header Access-Control-Max-Age 86400;
        return 204;
    }
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    proxy_pass http://backend;
}

Директива always обязательна: без неё Nginx не добавит заголовок к ответам с кодами ошибок, и вместо честного 500 фронтенд увидит невнятную CORS-ошибку.

Типичные грабли

  • * вместе с credentials не работает. Если запрос идёт с куками (credentials: "include"), браузер требует конкретный источник и Access-Control-Allow-Credentials: true. Звёздочка запрещена — подставляйте домен из заголовка Origin, сверяясь со списком разрешённых. Возвращать любой пришедший Origin без проверки — то же самое, что *, только выглядит аккуратнее.
  • Access-Control-Allow-Origin: * на приватном API. Это разрешение любому сайту дёргать ваш API из браузера пользователя. Для публичной read-only выдачи нормально, для личного кабинета — нет.
  • OPTIONS закрыт авторизацией. Preflight приходит без токена, поэтому 401 на OPTIONS ломает вообще все запросы. Метод должен отвечать до проверки авторизации.
  • Заголовки в ответе не видны JS. Кастомный заголовок вроде X-Total-Count нужно явно перечислить в Access-Control-Expose-Headers.
  • Дубли заголовка. Если CORS отдают и Nginx, и фреймворк, приходят два Access-Control-Allow-Origin — браузер отвергает такой ответ. Настраивайте в одном месте.

Связанные термины

  • HTTP — заголовки и метод OPTIONS, на которых построен CORS
  • Веб-разработка — где возникает разделение фронтенда и API
  • REST API — вызовы, вызывающие preflight
  • Nginx — где чаще всего настраивают заголовки
  • HAR-файл — чем диагностировать проблему

Готовы запустить GPU-задачу?

Запустить GPU-сервер