⏱️ Lectura: 14 min

Tu fetch funciona perfecto en Postman pero el navegador lo bloquea con un error rojo en la consola: bienvenido a CORS, la política que decide qué origen puede leer la respuesta de tu API. No es un bug de tu código ni un fallo de red: es el navegador cumpliendo una regla de seguridad que existe desde hace más de una década.

📑 En este artículo
  1. TL;DR
  2. Qué es CORS y por qué existe
  3. Cómo funciona CORS en detalle
  4. Ejemplos prácticos
  5. Cómo hacerlo paso a paso
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas
  9. Profundizando: lo que pasa por dentro
  10. Preguntas frecuentes
    1. ¿CORS es lo mismo que un firewall?
    2. ¿Por qué mi petición funciona en Postman pero falla en el navegador?
    3. ¿El usuario puede desactivar CORS en su propio navegador?
    4. ¿Necesito configurar CORS si el frontend y la API están en el mismo dominio?
    5. ¿Qué diferencia hay entre la cabecera Origin y Referer?
  11. Referencias

Entender CORS a fondo te ahorra horas de depuración inútil. En esta guía vas a ver cómo funciona el mecanismo por dentro, cómo configurarlo correctamente en un servidor real y cuáles son los errores que más se repiten al implementarlo.

TL;DR

  • Vas a entender qué es el Origin y por qué el navegador lo compara antes de entregar una respuesta.
  • Vas a distinguir una petición simple de una que dispara preflight OPTIONS.
  • Vas a configurar Access-Control-Allow-Origin, Allow-Methods, Allow-Headers y Allow-Credentials sin errores.
  • Vas a montar un servidor Express con el paquete cors y probarlo con curl paso a paso.
  • Vas a evitar el error clásico de combinar wildcard * con credentials: true.
  • Vas a verificar con las devtools si el preflight se cacheó gracias a Access-Control-Max-Age.
  • Vas a diferenciar CORS de un proxy inverso y de postMessage para cuando no controlás el backend.

Qué es CORS y por qué existe

CORS son las siglas de Cross-Origin Resource Sharing. Es un mecanismo del navegador, no del servidor ni de tu lenguaje de programación, que controla si un script cargado desde un origen puede leer la respuesta de una petición hecha a otro origen distinto.

Un origen se define por tres partes: protocolo, dominio y puerto. https://miapp.com y http://miapp.com son orígenes distintos porque cambia el protocolo. https://miapp.com y https://api.miapp.com también son distintos porque cambia el subdominio. Incluso https://miapp.com:3000 es un origen diferente de https://miapp.com por el puerto.

La razón de que esto exista es la política de mismo origen (same-origin policy), una regla de seguridad presente en todos los navegadores modernos desde los años noventa. Sin ella, cualquier sitio web podría incluir un script que llame silenciosamente a tu banco, tu correo o tu red social usando las cookies de sesión que ya tenés guardadas en el navegador, y leer la respuesta como si fuera el propio sitio.

CORS no bloquea la petición en sí: el servidor la recibe y puede procesarla igual. Lo que bloquea es que el navegador le entregue la respuesta al código JavaScript que la pidió, salvo que el servidor diga explícitamente, mediante cabeceras HTTP, que ese origen tiene permiso para leerla. Esta distinción es clave y confunde a mucha gente que empieza: el ataque no se evita impidiendo el envío, se evita impidiendo la lectura de la respuesta.

Cómo funciona CORS en detalle

Cuando un script hace una petición cross-origin, el navegador agrega automáticamente una cabecera Origin con el dominio desde el que se ejecuta el script. El servidor recibe esa cabecera y decide, con su propia lógica, si responde o no con Access-Control-Allow-Origin incluyendo ese mismo origen (o un wildcard *).

No todas las peticiones se tratan igual. El estándar Fetch de WHATWG divide las peticiones cross-origin en dos categorías: simples y las que requieren preflight.

CriterioPetición simplePetición con preflight
Métodos HTTPGET, HEAD, POSTPUT, DELETE, PATCH y otros
Content-Type permitidotext/plain, form-urlencoded, form-dataapplication/json y otros
Cabeceras personalizadasNo están permitidasAuthorization, X-Api-Key, etc.
Peticiones reales al servidor1 (la petición real)2 (OPTIONS de aviso + la real)

Para las peticiones que no califican como simples, el navegador envía primero una petición OPTIONS automática, llamada preflight, antes de mandar la petición real. En esa petición viaja Access-Control-Request-Method y Access-Control-Request-Headers, avisando qué va a intentar hacer la petición real. El servidor tiene que responder con las cabeceras Access-Control-Allow-Origin, Access-Control-Allow-Methods y Access-Control-Allow-Headers autorizando explícitamente eso.

sequenceDiagram
participant B as Navegador
participant S as Servidor API
B->>S: OPTIONS /datos (preflight)
Note over B,S: Origin, Access-Control-Request-Method
S-->>B: 204 con Access-Control-Allow-Origin
B->>S: GET /datos (peticion real)
S-->>B: 200 con los datos
Note over B,S: El navegador entrega la respuesta a JS
Diagrama de la política CORS bloqueando una petición entre dominios distintos
El navegador compara el Origin del script con el del servidor antes de entregar la respuesta. Foto de Matias Luge en Unsplash

Si el servidor no responde con las cabeceras correctas, el navegador nunca llega a mandar la petición real (en el caso de preflight) o manda la petición pero bloquea la lectura de la respuesta en JavaScript (en el caso de una petición simple). En ambos casos, la consola muestra un error de CORS y tu código recibe un TypeError genérico, no el cuerpo de la respuesta ni el código de estado real.

Ejemplos prácticos

Este es el caso más común: un frontend en un dominio llamando a una API en otro sin que el servidor tenga CORS configurado.

// Cliente en https://miapp.com intentando llamar a otra API
fetch('https://api.otroservicio.com/usuarios')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error('Fallo la peticion:', err));

// Consola del navegador:
// Access to fetch at 'https://api.otroservicio.com/usuarios' from origin
// 'https://miapp.com' has been blocked by CORS policy: No
// 'Access-Control-Allow-Origin' header is present on the requested resource.

El fetch se ejecuta y el servidor sí recibe la petición: podés verlo en los logs del backend. Pero el navegador nunca le entrega el JSON al .then(), y el .catch() recibe un error de red genérico sin detalle del código de estado.

La solución vive del lado del servidor, no del cliente. Acá un ejemplo realista con Express y el paquete cors:

// servidor.js (Node.js + Express)
const express = require('express');
const cors = require('cors');
const app = express();

const opcionesCors = {
  origin: 'https://miapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(opcionesCors));

app.get('/usuarios', (req, res) => {
  res.json([{ id: 1, nombre: 'Ana' }]);
});

app.listen(3000, () => console.log('API en puerto 3000'));

Con esta configuración, el middleware cors revisa la cabecera Origin de cada petición entrante, y si coincide con https://miapp.com agrega automáticamente Access-Control-Allow-Origin: https://miapp.com a la respuesta. El navegador ve esa cabecera y entrega el JSON al fetch del cliente sin bloquear nada.

flowchart TD
A["Peticion fetch"] --> B{"Mismo origen?"}
B -->|"Si"| C["Se envia directo"]
B -->|"No"| D{"Es peticion simple?"}
D -->|"Si"| E["Se envia y el navegador revisa la respuesta"]
D -->|"No"| F["Preflight OPTIONS primero"]
F --> G{"Servidor autoriza?"}
G -->|"Si"| H["Se envia la peticion real"]
G -->|"No"| I["Error CORS en consola"]

Cómo hacerlo paso a paso

Para reproducir el ejemplo anterior en tu máquina, instalá las dependencias:

npm install express cors

Guardá el código del servidor de arriba en servidor.js y ejecutalo con node servidor.js. La API queda escuchando en el puerto 3000.

Para verificar que la configuración de CORS responde correctamente sin depender del navegador, podés simular el preflight con curl:

curl -i -X OPTIONS http://localhost:3000/usuarios \n  -H "Origin: https://miapp.com" \n  -H "Access-Control-Request-Method: PUT" \n  -H "Access-Control-Request-Headers: Content-Type, Authorization"

La respuesta debe incluir Access-Control-Allow-Origin: https://miapp.com, Access-Control-Allow-Methods con PUT en la lista y Access-Control-Allow-Headers con Content-Type y Authorization. Si falta alguna de esas tres cabeceras, ese es el punto exacto que hay que corregir en la configuración del servidor.

Desde el navegador, la forma de confirmarlo es abrir las devtools, ir a la pestaña Network, filtrar por el nombre del endpoint y revisar dos entradas: la petición OPTIONS (el preflight) y la petición real. Si el preflight tiene status 204 o 200 con las cabeceras correctas, la petición real debería completarse sin error de CORS.

💡 Tip: Usá Access-Control-Max-Age para que el navegador cachee el resultado del preflight y no repita el OPTIONS en cada petición durante ese tiempo.

Casos de uso reales

El escenario más frecuente es un frontend en desarrollo local, por ejemplo http://localhost:3001, hablando con una API que corre en http://localhost:3000. Aunque ambos estén en la misma máquina, el puerto distinto ya los convierte en orígenes diferentes para el navegador.

En producción, es común tener el frontend servido desde un dominio raíz y la API en un subdominio, como app.miempresa.com contra api.miempresa.com. Ahí CORS es obligatorio porque son orígenes distintos aunque compartan el dominio principal.

Otro caso típico son las APIs públicas que sirven a terceros, como una API de clima o de tipos de cambio: suelen responder Access-Control-Allow-Origin: * para cualquier consumidor, porque no manejan cookies de sesión ni datos sensibles por usuario.

Las fuentes web (@font-face) también dependen de CORS cuando se sirven desde un CDN distinto al del sitio: por eso a veces hace falta el atributo crossorigin en la etiqueta que carga la fuente.

Errores comunes y buenas prácticas

El error más repetido es combinar Access-Control-Allow-Origin: * con Access-Control-Allow-Credentials: true. El navegador rechaza esa combinación directamente, sin importar qué diga el resto de la respuesta, porque expondría cookies de sesión a cualquier origen del mundo.

⚠️ Ojo: Si tu petición usa credentials: ‘include’ en el fetch, el servidor tiene que responder con un origen explícito en Access-Control-Allow-Origin, nunca con el wildcard *.

Otro error frecuente es olvidar que el servidor necesita manejar explícitamente el método OPTIONS. Muchos frameworks minimalistas no lo hacen por defecto, y si tu ruta solo define un handler para GET o POST, la petición OPTIONS cae en un 404 y el preflight falla antes de llegar a la petición real.

También es común confundir un error de CORS con un error de red genérico. Si el servidor está caído o la URL está mal escrita, el navegador también muestra un fallo en el fetch, pero sin el texto específico “blocked by CORS policy” en la consola. Revisar el mensaje exacto ahorra tiempo de diagnóstico.

Por último, usar mode: 'no-cors' en el cliente no soluciona el problema: silencia el error, pero la respuesta que recibís es opaca (status 0, sin body legible), así que tu código igual no puede leer los datos.

Comparativa con alternativas

CORS no es la única forma de resolver una llamada cross-origin, aunque sí es la más estándar cuando controlás el backend.

EnfoqueCuándo usarloVentajaLimitación
CORSAPI propia que controlásEstándar, seguro, granular por origenRequiere configurar el servidor
Proxy inversoAPI de terceros sin CORSEvita tocar el servidor externoAgrega infraestructura y latencia
JSONPAPIs legacy muy antiguasFunciona sin fetch ni CORSSolo GET, riesgo de seguridad, obsoleto
postMessageComunicación entre iframes o ventanasNo pasa por HTTP, es directo en el navegadorNo sirve para llamar a un backend

Cuando no controlás el servidor de terceros y este no responde con cabeceras CORS, la salida práctica suele ser un proxy inverso propio: tu backend llama a la API externa server-to-server (donde CORS no aplica, porque es una restricción exclusiva de navegadores) y tu frontend le habla solo a tu proxy, que está en tu mismo origen.

Profundizando: lo que pasa por dentro

CORS no es un mecanismo de autenticación ni reemplaza la autorización del lado del servidor. Un atacante puede llamar a tu API directamente con curl o Postman sin pasar nunca por un navegador, así que CORS mal configurado no es la única capa de defensa que necesitás: tokens, sesiones validadas y control de acceso por endpoint siguen siendo obligatorios.

💭 Clave: CORS protege al usuario del navegador, no protege tu API. La autenticación y autorización del lado del servidor siguen siendo obligatorias con o sin CORS.

Hay una relación directa entre CORS y CSRF (Cross-Site Request Forgery). Antes de que CORS existiera, los navegadores ya permitían mandar formularios cross-origin sin restricción, lo cual habilitaba ataques CSRF clásicos. CORS restringe la lectura de la respuesta, pero no impide que una petición simple, como un POST con form-urlencoded, se envíe igual. Por eso los tokens anti-CSRF siguen siendo necesarios en endpoints sensibles que aceptan peticiones simples.

Otro nivel avanzado son las cabeceras de aislamiento cross-origin, como Cross-Origin-Resource-Policy (CORP) y Cross-Origin-Embedder-Policy (COEP), que restringen qué recursos puede embeber una página incluso más allá de lo que cubre CORS, y que Chrome y Firefox exigen para habilitar APIs sensibles como SharedArrayBuffer.

También existe Access-Control-Allow-Private-Network, una extensión más reciente pensada para cuando una página pública en internet intenta llamar a un servicio dentro de una red privada o localhost, un patrón cada vez más común con herramientas de desarrollo local que exponen APIs.

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: levantá el servidor Express de este artículo, quitale la línea app.use(cors(opcionesCors)) y observá el error exacto que aparece en la consola del navegador al llamar al endpoint desde otro origen.

Preguntas frecuentes

¿CORS es lo mismo que un firewall?

No. Un firewall filtra tráfico de red a nivel de servidor o infraestructura. CORS es una restricción que aplica exclusivamente el navegador del usuario final; el servidor recibe la petición igual, solo cambia si el navegador le muestra la respuesta al script que la pidió.

¿Por qué mi petición funciona en Postman pero falla en el navegador?

Porque Postman no es un navegador y no aplica la política de mismo origen. CORS es una restricción del entorno navegador, no del protocolo HTTP en sí, así que herramientas como Postman, curl o un backend llamando a otro backend nunca la sufren.

¿El usuario puede desactivar CORS en su propio navegador?

Técnicamente sí, con flags experimentales o extensiones, pero eso no soluciona nada para el resto de tus usuarios. CORS se configura del lado del servidor porque es ahí donde tenés control real sobre quién accede a los datos.

¿Necesito configurar CORS si el frontend y la API están en el mismo dominio?

No, si comparten protocolo, dominio y puerto exactos, el navegador los trata como mismo origen y no aplica ninguna restricción de CORS.

¿Qué diferencia hay entre la cabecera Origin y Referer?

Origin solo indica el esquema, dominio y puerto del solicitante, sin ruta ni query string, y es la que usa CORS para decidir. Referer incluye la URL completa de origen y se usa con otros fines, como analítica o protección adicional contra CSRF, pero no participa en la decisión de CORS.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de Valery Sysoev en Unsplash


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.