Menu

CORS en Express: solucionar el error CORS (con y sin cors)

Un error CORS significa que el navegador impidió que una página leyera la respuesta de tu API porque esa respuesta no permitía el origen de la página. Se arregla en el servidor enviando Access-Control-Allow-Origin y respondiendo a las peticiones preflight OPTIONS, a mano o con el paquete cors.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Un error CORS ("No 'Access-Control-Allow-Origin' header is present") significa que una página web de un origen, como http://localhost:5173, llamó a tu API en otro origen, y el navegador no dejó que la página leyera la respuesta porque la respuesta no decía que ese origen estuviera permitido. La solución está en el servidor: envía Access-Control-Allow-Origin con el origen de la página, y responde a la petición preflight OPTIONS del navegador con los métodos y headers que aceptas. En Express son unas pocas líneas de middleware, o app.use(cors()) del paquete cors.

La respuesta es la lista de productos, y su pestaña Headers muestra access-control-allow-origin: https://app.example.com: una página de ese origen puede leerla. Ahora envía OPTIONS /api/products/1 con los headers Origin: https://app.example.com y Access-Control-Request-Method: PUT. Ese es el preflight que envía un navegador antes de un PUT, y la respuesta es 204 con los métodos y headers permitidos y sin body.

Qué comprueba el navegador

Las páginas del mismo origen no necesitan nada. Para una petición entre orígenes, el navegador lee estos headers de la respuesta:

HeaderSignificado
Access-Control-Allow-OriginEl único origen que puede leer la respuesta, o * para cualquiera
Access-Control-Allow-MethodsMétodos permitidos en la petición real (solo en el preflight)
Access-Control-Allow-HeadersHeaders que la página puede enviar, como Authorization (solo en el preflight)
Access-Control-Allow-Credentialstrue si la página puede enviar cookies y leer la respuesta
Access-Control-Max-AgeSegundos que el navegador puede cachear la respuesta del preflight
Vary: OriginIndica a las cachés que la respuesta depende del header Origin de la petición

Un GET simple o un POST tipo formulario va directo al servidor, y el navegador comprueba Access-Control-Allow-Origin en la respuesta. El servidor ejecuta la petición de todas formas; CORS solo decide si la página ve el resultado. Una petición con un body JSON, un header Authorization o un método como PUT o DELETE va precedida de una petición preflight OPTIONS, y la petición real solo se envía si la respuesta del preflight lo permite. Por eso un middleware CORS tiene que responder a OPTIONS antes de que se ejecute cualquier ruta o comprobación de auth.

Permitir varios orígenes, con credenciales

Access-Control-Allow-Origin contiene un origen o *, nunca una lista. Para permitir varios, compara el header Origin de la petición con una lista de permitidos y devuélvelo cuando coincida:

La primera petición no tiene header Origin, como una llamada desde curl o desde una página del mismo origen, así que recibe {"origin":null} y ningún header CORS. Envía GET /api/me con el header Origin: http://localhost:5173 y la pestaña Headers muestra ese origen devuelto más access-control-allow-credentials: true. Cambia el header a Origin: https://evil.example y los dos headers CORS desaparecen: el servidor sigue respondiendo, pero un navegador impediría que la página lo leyera. Access-Control-Allow-Credentials: true permite que la página envíe cookies (con fetch(url, { credentials: 'include' })), y los navegadores lo rechazan junto con *, así que las credenciales siempre necesitan el origen exacto.

El paquete cors

El paquete cors hace el mismo trabajo con opciones. Instálalo con npm install cors:

const cors = require('cors');

// Any origin, no cookies: fine for a public read-only API.
app.use(cors());

// Only your frontend, with cookies.
app.use(cors({
    origin: ['https://app.example.com', 'http://localhost:5173'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

// One route only.
app.get('/api/public', cors(), handler);

Llamado sin opciones, cors() envía Access-Control-Allow-Origin: * y responde a todos los preflight. Usa * solo para datos públicos que no necesitan login.

Errores comunes

Añadir CORS después de las rutas o después de un middleware de auth. La petición preflight OPTIONS no lleva header Authorization, así que una comprobación de auth puesta antes la responde con 401 y el navegador muestra un error CORS. Registra primero el middleware CORS.

Poner * con credenciales. Los navegadores rechazan la respuesta. Devuelve el origen concreto.

Intentar arreglar CORS en el frontend. Opciones de la petición como mode: 'no-cors' no quitan la restricción; te dan una respuesta opaca que no puedes leer. Solo lo arreglan los headers del servidor.

Para ver cómo funcionan app.use() y el orden del middleware, consulta middleware en Express.

Preguntas frecuentes

¿Por qué mi API funciona en Postman o curl pero no en el navegador?

CORS solo lo aplican los navegadores. Postman, curl y otros servidores ignoran los headers CORS, así que leen la respuesta igualmente. El navegador se niega a entregar la respuesta a los scripts de una página de otro origen salvo que el servidor permita ese origen.

¿CORS es una medida de seguridad para mi servidor?

Protege a los usuarios, no a tu servidor. Impide que una página maliciosa lea las respuestas de tu API con las cookies del usuario. Cualquiera puede seguir llamando a tu API desde un script o un servidor, así que la autenticación y la autorización siguen siendo cosa tuya.

¿Qué cuenta como un origen distinto?

Un origen es el esquema, el host y el puerto juntos. http://localhost:5173 y http://localhost:3000 son orígenes distintos porque cambia el puerto, y https://example.com y https://api.example.com se diferencian en el host.

¿Puedo evitar CORS en desarrollo?

Sí, haciendo que la petición sea del mismo origen: configura el servidor de desarrollo del frontend (server.proxy de Vite, por ejemplo) para que reenvíe /api al servidor de Express. Así el navegador habla con un solo origen y no hacen falta headers CORS.

Ilustración de los lenguajes de programación de Coddy

Aprende a programar con Coddy

Comenzar