// Engineering Log

HTTPS y servidor web en la práctica: Parte 3 — Qué es CORS: errores en la consola del navegador y configuración en Nginx

Publicado el 02.11.2026 · Autor:

// Ruta rapida

Este articulo pertenece al tema Servidores e infraestructura.

CORS (Cross-Origin Resource Sharing) — mecanismo mediante el cual el servidor permite que el código JavaScript de otros sitios lea sus respuestas. Por defecto el navegador no lo permite: un script en https://app.example.ru puede enviar una solicitud a https://api.example.ru, pero no podrá leer la respuesta si el servidor del API no lo permite explícitamente. De ahí aparecen errores del tipo «blocked by CORS policy» en la consola, que surgen tan pronto como el frontend y el API están en direcciones distintas. A continuación — cómo funciona la comprobación, qué significa cada error típico y cómo configurar el servidor. Los propios encabezados HTTP están explicados brevemente en el artículo «Encabezados HTTP: cuáles existen y qué hacen».

Qué es el «origen»

El origen (origin) es esquema, host y puerto juntos. Dos direcciones pertenecen al mismo origen solo si las tres partes coinciden:

Dirección de la páginaDirección de la solicitud¿Mismo origen?
https://example.ruhttps://example.ru/apisí
https://example.ruhttps://api.example.runo — distinto host
https://example.ruhttp://example.runo — distinto esquema
http://localhost:3000http://localhost:8000no — distinto puerto

Un subdominio ya es otro origen. El desarrollo local con frontend en el puerto 3000 y API en 8000 también.

Qué es exactamente lo que bloquea el navegador

La política de mismo origen (same-origin policy) prohíbe que un script lea respuestas de orígenes ajenos. En la mayoría de los casos no prohíbe enviar solicitudes. CORS es la manera de relajar esta regla para orígenes concretos.

De aquí tres consecuencias importantes:

  • CORS protege al usuario, no al servidor. curl, Postman, un script en Python u otro servidor no verifican CORS y siempre obtienen la respuesta. Restringir el acceso al API solo es posible con autenticación;
  • Una solicitud «simple» desde un sitio tercero llega al servidor y se ejecuta — el navegador solo no entrega la respuesta al script. Lo que protege contra la falsificación de tales solicitudes no es CORS, sino tokens CSRF y SameSite en las cookies («Ataques a nivel HTTP»);
  • Un error de CORS es una respuesta del servidor sin los encabezados necesarios, no una falla de red. El servidor puede haber devuelto 200 o 500: en la pestaña Network se ve el código real.

Solicitudes simples y preflight

Un navegador envía una solicitud simple inmediatamente y comprueba los encabezados de la respuesta. Una solicitud se considera simple si simultáneamente:

  • el método es GET, HEAD o POST;
  • los encabezados son solo de una lista corta permitida (Accept, Accept-Language, Content-Language, Content-Type y algunos otros), sin Authorization ni X-… personalizados;
  • Content-Type es solo application/x-www-form-urlencoded, multipart/form-data o text/plain.

Cualquier otra solicitud — PUT, PATCH, DELETE, JSON con Content-Type: application/json, solicitud con Authorization — requiere una solicitud previa (preflight). El navegador primero envía un 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

El servidor debe responder con un código 2xx y permisos:

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

Solo después de esto se envía el PUT real, y su respuesta también debe contener Access-Control-Allow-Origin. Access-Control-Max-Age permite al navegador cachear el resultado de la preflight; Chrome limita este tiempo a dos horas.

Encabezados de respuesta

  • Access-Control-Allow-Origin — qué origen puede leer la respuesta: concreto (https://app.example.ru) o cualquiera (*). Solo un valor: no se permite una lista separada por comas;
  • Access-Control-Allow-Methods, Access-Control-Allow-Headers — métodos y encabezados permitidos, solo en la respuesta a OPTIONS;
  • Access-Control-Allow-Credentials: true — permitir solicitudes con cookie y Authorization;
  • Access-Control-Expose-Headers — qué encabezados de la respuesta son accesibles para el script. Por defecto el script ve solo unos pocos básicos (Content-Type, Content-Length, Cache-Control, etc.); para leer X-Total-Count o Content-Disposition hay que listarlos;
  • Access-Control-Max-Age — cacheo de la preflight, en segundos;
  • Vary: Origin — obligatorio si Allow-Origin depende del Origin de la solicitud. Si no, un CDN o el navegador cacheará la respuesta con permiso para un sitio y se la dará a otro.

Por defecto fetch a un origen ajeno no envía cookies. Para que lo haga:

javascript
fetch("https://api.example.ru/v1/me", { credentials: "include" });

Entonces el servidor debe:

  • devolver un origen concreto en Access-Control-Allow-Origin — * está prohibido junto con credenciales;
  • devolver Access-Control-Allow-Credentials: true;
  • la cookie debe tener SameSite adecuado: para otro sitio — SameSite=None; Secure. Para subdominios de un mismo sitio (app.example.ru y api.example.ru — un «sitio» desde el punto de vista de SameSite) basta con Lax.

Un token en el encabezado Authorization es más sencillo que la cookie: no hace falta credentials: "include", pero sí que Authorization aparezca en Access-Control-Allow-Headers.

Errores en la consola y qué significan

No 'Access-Control-Allow-Origin' header is present on the requested resource El servidor no devolvió el encabezado. Razones: CORS no está configurado; está configurado, pero la respuesta vino con error (404, 500, 502) y los encabezados se añaden solo en respuestas exitosas — en Nginx falta always; la solicitud fue enviada a la dirección equivocada. Mire el código real en la pestaña Network.

Response to preflight request doesn't pass access control check: It does not have HTTP ok status La respuesta a OPTIONS no es 2xx. Lo más habitual es que el OPTIONS requiera autenticación (401), no esté soportado por el framework (404, 405) o redirija. La preflight siempre va sin cookie ni Authorization — el servidor debe responder a ella sin comprobar el acceso.

Redirect is not allowed for a preflight request El OPTIONS devolvió una redirección: de http a https, de con slash a sin slash (/v1/orders → /v1/orders/), a la página de login. Corrija la dirección en el frontend para evitar la redirección.

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' Solicitud con cookie, y en la respuesta *. Se necesita un origen concreto y Allow-Credentials: true.

Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response El encabezado no está listado en Access-Control-Allow-Headers. Lo mismo para content-type al enviar JSON y para cualquier encabezado personalizado.

Method PUT is not allowed by Access-Control-Allow-Methods in preflight response El método no está listado en Access-Control-Allow-Methods.

The 'Access-Control-Allow-Origin' header contains multiple values '…, …', but only one is allowed El encabezado lo añaden dos capas a la vez — por ejemplo, Nginx y la aplicación, o la aplicación y un CDN. Deje CORS en un solo lugar.

La respuesta llega, pero response.headers.get("X-Total-Count") devuelve null El encabezado no está listado en Access-Control-Expose-Headers.

Configuración en Nginx

Orígenes permitidos — en una lista con map, para no reflejar cualquier Origin:

nginx
map $http_origin $cors_origin {
    default "";
    "https://app.example.ru"   $http_origin;
    "https://admin.example.ru" $http_origin;
    "~^http://localhost:\d+$"  $http_origin;   # solo para el entorno de pruebas
}

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;
    }
}

Aclaraciones:

  • El if con return dentro de location — uno de los usos permitidos de if en Nginx;
  • si el origen no está en la lista, $cors_origin queda vacío, y Nginx no emite un encabezado con valor vacío — el navegador bloqueará la lectura de la respuesta;
  • always es necesario para que los encabezados estén también en respuestas 4xx/5xx — de lo contrario, en lugar de un error comprensible del API el frontend verá un error de CORS;
  • si en location aparece otro add_header, los encabezados a nivel server dejarán de aplicarse en él — mantenga todos los encabezados en un solo lugar.

Configuración en la aplicación

Si CORS se configura en la aplicación, Nginx no añade encabezados en absoluto:

  • Express: paquete 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 con allow_origins, allow_credentials, allow_methods, allow_headers;
  • Spring: @CrossOrigin o un CorsConfigurationSource global.

El middleware de CORS en la aplicación debe estar antes de la autenticación, de lo contrario OPTIONS recibirá 401.

Cómo prescindir de CORS

La solución más simple es no hacer solicitudes cross-domain:

  • un dominio para frontend y API: https://example.ru/ — el sitio, https://example.ru/api/ — el API a través del mismo Nginx. El origen es el mismo, CORS no es necesario, las cookies funcionan sin SameSite=None;
  • proxy en desarrollo: en Vite — server.proxy, en Create React App — el campo proxy en package.json. El frontend hace llamadas a /api en su propio puerto, y el servidor de desarrollo reenvía las solicitudes al API.

Cómo comprobar

La preflight se puede reproducir con curl:

bash
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'

Y la principal — con el mismo Origin:

bash
curl -si https://api.example.ru/v1/orders -H 'Origin: https://app.example.ru' | grep -i '^access-control'

Con un origen que no esté en la lista no debe aparecer el encabezado Access-Control-Allow-Origin.

Errores típicos

  • Access-Control-Allow-Origin se toma del Origin de la solicitud sin comprobarlo contra una lista — eso permite cualquier sitio, además con cookies;
  • Access-Control-Allow-Origin: * en un API con autenticación por cookie — no funciona y revela que CORS se configuró a la ligera;
  • Falta Vary: Origin en una respuesta que depende de Origin — el CDN sirve una respuesta con permiso para un origen ajeno;
  • CORS está configurado tanto en Nginx como en la aplicación — encabezado duplicado;
  • OPTIONS protegido por autenticación o limitación de frecuencia — todo el frontend se rompe bajo cualquier carga;
  • Intentar «arreglar CORS» en el frontend (mode: "no-cors") — la solicitud sale, pero la respuesta queda totalmente inaccesible para el script.

// Tarea parecida

Si estas resolviendo algo parecido

Este articulo pertenece a uno de los temas principales de trabajo. Puedes seguir leyendo sobre el tema, ir a la pagina principal para entender a que me dedico o abrir directamente los servicios.

Tema del articulo

Servidores e infraestructura

VPS, Linux, stack web, migraciones, hosting, bases de datos y operacion base.

Tareas frecuentes de esta tema

  • Migrar un sitio o servicio a un nuevo servidor
  • Configurar Linux, Nginx, base de datos y copias de seguridad
  • Entender por que el sistema funciona de forma inestable

// Siguiente paso

Si necesitas ayuda con este tema y no solo otro articulo, es mejor ir directo a la pagina del servicio. La pagina principal y la seleccion de materiales quedan como rutas secundarias.

Abrir servicios

// Reviews

Reseñas relacionadas

apande

apande

Configuración de Nginx y OpenCart

07.09.2024 · ★ 5/5

Un comprador muy poderoso
kireevk

kireevk

Diagnóstico de Nginx Proxy Manager en un contenedor Docker y solución del problema

15.03.2024 · ★ 5/5

Comprador acostumbrado

// Contact

¿Necesitas ayuda?

Escríbeme y te ayudaré a resolver el problema

Respondo en un día laborable (03:00-13:00 GMT)

O deje su solicitud aquí:

Confirme que no es un bot.

Escribir y recibir una respuesta rápida