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.
