CORS y Same-Origin Policy en JavaScript: preflight, credentials, headers y errores comunes

CORS (Cross-Origin Resource Sharing) y la Same-Origin Policy son los mecanismos que los navegadores usan para controlar qué peticiones pueden hacerse entre orígenes distintos. Entenderlos a fondo evita horas de depuración y permite configurar correctamente tanto el servidor como el cliente.

La Same-Origin Policy

Un origen se define por el esquema, el host y el puerto. Dos URLs son del mismo origen solo si los tres coinciden exactamente:

// Mismo origen que https://app.ejemplo.com:443
// ? https://app.ejemplo.com/api/datos       (mismo esquema, host, puerto)
// ? http://app.ejemplo.com/api              (esquema diferente)
// ? https://api.ejemplo.com/datos           (host diferente)
// ? https://app.ejemplo.com:8080/datos      (puerto diferente)

// La SOP bloquea por defecto:
fetch('https://api.otro-dominio.com/datos') // Bloqueado sin CORS correcto
  .then(r => r.json())
  .catch(err => console.error('CORS error:', err));

Peticiones simples vs preflight

No todas las peticiones cross-origin disparan una solicitud preflight. Las peticiones "simples" usan un conjunto restringido de métodos y headers:

// Petición SIMPLE — no dispara preflight
fetch('https://api.ejemplo.com/datos', {
  method: 'GET',              // GET, HEAD, POST con ciertos Content-Type
  headers: {
    'Content-Type': 'text/plain', // o application/x-www-form-urlencoded
  }
});

// Petición NO simple — dispara OPTIONS preflight primero
fetch('https://api.ejemplo.com/datos', {
  method: 'PUT',              // Método no simple
  headers: {
    'Content-Type': 'application/json', // Header no simple
    'Authorization': 'Bearer token',     // Header personalizado
  },
  body: JSON.stringify({ nombre: 'Ana' }),
});

// Antes de la petición PUT, el navegador envía automáticamente:
// OPTIONS /datos HTTP/1.1
// Origin: https://app.ejemplo.com
// Access-Control-Request-Method: PUT
// Access-Control-Request-Headers: content-type, authorization

Configuración CORS en el servidor

El servidor debe responder con los headers de CORS correctos tanto en el preflight como en la petición real:

// Node.js / Express — configuración CORS manual
app.use((req, res, next) => {
  const origenesPermitidos = [
    'https://app.ejemplo.com',
    'https://admin.ejemplo.com',
  ];

  const origen = req.headers.origin;
  if (origenesPermitidos.includes(origen)) {
    res.setHeader('Access-Control-Allow-Origin', origen);
    res.setHeader('Vary', 'Origin'); // Importante para cachés
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header');
  res.setHeader('Access-Control-Max-Age', '86400'); // Cachear preflight 24h

  // Responder al preflight
  if (req.method === 'OPTIONS') {
    return res.status(204).end();
  }

  next();
});

// Con la librería cors (más cómodo):
const cors = require('cors');
app.use(cors({
  origin: ['https://app.ejemplo.com', 'https://admin.ejemplo.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,
}));

Credenciales: cookies y autenticación cross-origin

Por defecto, las peticiones cross-origin no incluyen cookies ni headers de autenticación. Para habilitarlo se necesita configuración tanto en el cliente como en el servidor:

// CLIENTE: indicar que se quieren incluir credenciales
fetch('https://api.ejemplo.com/perfil', {
  credentials: 'include', // Incluir cookies y auth headers
  headers: {
    'Content-Type': 'application/json',
  },
});

// Con XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'https://api.ejemplo.com/perfil');
xhr.send();

// SERVIDOR: se requieren estos headers específicos
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: https://app.ejemplo.com  ? NO puede ser '*' con credentials

// INCORRECTO con credentials:
res.setHeader('Access-Control-Allow-Origin', '*'); // Error — credenciales no funcionan con wildcard

// CORRECTO con credentials:
res.setHeader('Access-Control-Allow-Origin', req.headers.origin); // Origen específico
res.setHeader('Access-Control-Allow-Credentials', 'true');

Exponer headers de respuesta al cliente

Por defecto, solo un subconjunto de headers de respuesta son accesibles desde JavaScript. Para exponer headers adicionales se usa Access-Control-Expose-Headers:

// Servidor:
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page, X-Rate-Limit-Remaining');
res.setHeader('X-Total-Count', '1234');
res.setHeader('X-Page', '2');

// Cliente — ahora estos headers son accesibles:
const res = await fetch('https://api.ejemplo.com/productos');
console.log(res.headers.get('X-Total-Count'));         // '1234'
console.log(res.headers.get('X-Rate-Limit-Remaining')); // valor del header
// Sin exponer, devuelven null aunque el servidor los envíe

Diagnosticar errores de CORS en la consola

Los mensajes de error de CORS en la consola del navegador suelen ser crípticos. Estos son los más frecuentes y su causa:

// "Access to fetch at '...' from origin '...' has been blocked by CORS policy:
//  No 'Access-Control-Allow-Origin' header is present"
// ? El servidor no devuelve el header CORS. Problema en el servidor.

// "has been blocked by CORS policy: 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'"
// ? Usar credentials con wildcard. Cambiar a origen específico en el servidor.

// "Response to preflight request doesn't pass access control check:
//  Method 'PUT' is not allowed"
// ? Faltan métodos en Access-Control-Allow-Methods del servidor.

// Cómo inspeccionar la petición preflight en DevTools:
// Network tab ? filtrar por "preflight" o "OPTIONS" ? ver headers de respuesta

CORS en desarrollo local

// Evitar problemas de CORS en desarrollo con proxy en Vite
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.ejemplo.com',
        changeOrigin: true,
        rewrite: path => path.replace(/^/api/, ''),
      },
    },
  },
};

// Las peticiones a /api/datos se reescriben como https://api.ejemplo.com/datos
// sin pasar por el navegador — no hay CORS en server-to-server

La clave para resolver problemas de CORS es entender que es siempre una restricción del navegador y que la solución está siempre en el servidor (o en el proxy de desarrollo). Nunca en el cliente. Un fetch sin el header correcto en la respuesta del servidor fallará siempre, independientemente de lo que se haga en el código JavaScript.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP