CORS explicado: preflight, cabeceras y cómo solucionar errores CORS
CORS explicado
CORS explicado: preflight, cabeceras, errores y soluciones | We.Inc
Cómo funciona CORS: peticiones simples y preflight, cada cabecera Access-Control, reglas de credenciales, los errores de consola habituales y una solución en el servidor para Express, Next.js y nginx.
CORS (intercambio de recursos de origen cruzado) es la forma en que un servidor le dice al navegador que el JavaScript de otro origen puede leer sus respuestas. El servidor responde con Access-Control-Allow-Origin y cabeceras relacionadas. Las peticiones con otros métodos, cabeceras personalizadas o cuerpos JSON envían antes un preflight OPTIONS. La solución siempre está en el servidor al que llamas, nunca en la página.
Las cabeceras de respuesta CORS de un vistazo
Cabecera
Qué permite
Access-Control-Allow-Origin
Qué origen puede leer la respuesta (un origen o *)
Access-Control-Allow-Methods
Métodos permitidos tras un preflight
Access-Control-Allow-Headers
Cabeceras de petición permitidas tras un preflight
Access-Control-Allow-Credentials
Cookies y autenticación enviadas con la petición (el valor debe ser true)
Access-Control-Expose-Headers
Cabeceras de respuesta que el script puede leer
Access-Control-Max-Age
Segundos que se puede guardar en caché la respuesta a un preflight
Un error CORS parece indicar que tu código está roto, pero normalmente la petición llegó al servidor sin problema. El navegador simplemente se negó a entregar la respuesta a tu script. El simulador de abajo te permite describir una petición y la configuración del servidor, y te dice si se lanza un preflight, si el navegador deja pasar la respuesta y qué mensaje verías en la consola.
Define el origen de la página, el método, cualquier cabecera personalizada y si se envían cookies.
Define lo que responde el servidor: origen permitido, métodos, cabeceras y el indicador de credenciales.
Lee el veredicto: si hace falta un preflight, si se aprueba y el motivo en consola si falla.
Copia el fragmento de servidor correspondiente de abajo y añádelo a la API, no al front end.
Devuelve el origen exacto que hace la petición desde una lista de permitidos cuando hay cookies de por medio, porque el comodín se rechaza con credenciales.
Añade Vary: Origin siempre que el origen permitido cambie según la petición, para que las cachés no sirvan la respuesta de un sitio a otro.
Responde a OPTIONS con un estado 2xx y sin redirección. Un preflight que acaba en una redirección al login falla siempre.
Desactivar CORS en tu propio navegador solo oculta el problema en tu máquina. Los visitantes siguen viendo el error.
Las llamadas de servidor a servidor nunca implican CORS. Si no controlas la API, llámala desde tu propio backend.
Preguntas frecuentes
¿Por qué funciona en Postman o curl y falla en el navegador?
CORS solo lo aplican los navegadores, para impedir que los scripts de un sitio lean los datos de otro sitio con tus cookies. Herramientas como curl no ejecutan código de páginas no confiables, así que lo ignoran. La respuesta del servidor era idéntica; solo el navegador decidió retenerla.
¿Qué provoca una petición preflight?
Cualquier método distinto de GET, HEAD o POST, cualquier cabecera fuera de la pequeña lista segura (Accept, Accept-Language, Content-Language, Content-Type, Range), o un Content-Type distinto de form encoding, multipart o text/plain. Enviar application/json es la causa más común.
¿Puedo solucionar CORS desde el frontend?
No. Poner cabeceras Access-Control en tu petición no sirve de nada, y mode: no-cors te da una respuesta opaca que tu script no puede leer. O la API añade las cabeceras, o tu propio servidor obtiene los datos y te los pasa.
¿Puedo usar Access-Control-Allow-Origin: * con cookies?
No. Cuando una petición lleva credenciales, el navegador rechaza un origen comodín y exige el origen exacto más Access-Control-Allow-Credentials: true. Lo mismo vale para los comodines en las listas de métodos, cabeceras y cabeceras expuestas.
¿Por qué mi script no puede leer una cabecera de respuesta personalizada?
Por defecto, un script de origen cruzado solo ve una lista corta de cabeceras de respuesta seguras. Incluye las demás en Access-Control-Expose-Headers, por ejemplo X-Request-Id, y pasarán a ser legibles.
Mismo origen, y por qué el navegador bloquea las lecturas
Un origen es el esquema, el host y el puerto juntos. https://app.example.com y https://api.example.com son orígenes distintos, y también lo son las versiones http y https de un mismo dominio. Por defecto, un script puede enviar una petición a otro origen, pero no puede leer lo que vuelve.
CORS es la autorización explícita. La API indica en qué orígenes confía y el navegador aplica esa lista en nombre del visitante. Por eso la cabecera debe venir del servidor al que llamas.
Peticiones simples y preflights
Un GET normal, o un POST desde un formulario corriente, sale directamente con una cabecera Origin. Después el navegador comprueba que la respuesta incluya un origen permitido antes de entregarla a tu código.
Cualquier cosa más allá de eso envía primero una petición OPTIONS con Access-Control-Request-Method y Access-Control-Request-Headers. Solo si el servidor aprueba ambas sale la petición real. Access-Control-Max-Age permite al navegador recordar la aprobación, aunque los navegadores limitan cuánto tiempo.
Cómo leer el error de la consola
Por seguridad, tu JavaScript solo sabe que el fetch falló, con un TypeError y sin detalles. El motivo real aparece en la consola del navegador, y nombra la cabecera que faltaba o estaba mal. Compáralo con la tabla de mensajes de abajo.
Errores CORS comunes y cómo solucionarlos
El texto del mensaje varía según el navegador. Estos son los motivos tal como los nombra Firefox en MDN; Chrome dice lo mismo con frases más largas.
Motivos CORS en la consola y la solución en el servidor
Motivo en la consola
Qué significa
Solución en el servidor
CORS header 'Access-Control-Allow-Origin' missing
La respuesta no llevaba ninguna cabecera allow-origin
Añade la cabecera a todas las respuestas, también a las de error
CORS header 'Access-Control-Allow-Origin' does not match
La cabecera indica un origen distinto
Devuelve el origen del llamante desde tu lista de permitidos
Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'
Se enviaron cookies pero el servidor respondió con *
Devuelve el origen exacto y Allow-Credentials: true
expected 'true' in CORS header 'Access-Control-Allow-Credentials'
Se enviaron credenciales pero falta el indicador
Envía Access-Control-Allow-Credentials: true
Did not find method in CORS header 'Access-Control-Allow-Methods'
El preflight no aprobó el método
Incluye PUT, PATCH o DELETE en Allow-Methods
missing token in CORS header 'Access-Control-Allow-Headers' from CORS preflight channel
Una cabecera de petición como Authorization no fue aprobada
Inclúyela en Allow-Headers
CORS preflight channel did not succeed
OPTIONS falló, redirigió o devolvió un error
Responde a OPTIONS con 204 y las cabeceras
Multiple CORS header 'Access-Control-Allow-Origin' not allowed
Dos capas (app y proxy) añadieron la cabecera
Defínela en un solo sitio
Fragmentos de servidor
Express: lista de permitidos con credenciales
Devuelve el origen solo si está en la lista, responde a los preflights con 204 y añade Vary para que las cachés mantengan separados los orígenes.
Route handler de Next.js
Un endpoint público y sin cookies puede usar el comodín. Exporta OPTIONS para que el preflight tenga su propia respuesta.
nginx delante de una API
El indicador always mantiene la cabecera también en las respuestas de error, así un 500 muestra su estado real en lugar de un error CORS.
El lado del navegador: enviar cookies entre orígenes
credentials: "include" es lo que activa en el servidor las reglas de credenciales más estrictas.
Simulador de peticiones CORS
Describe una petición y las cabeceras CORS del servidor para ver si se envía un preflight y si el navegador deja que tu script lea la respuesta.
La petición
La respuesta del servidor
Origen de la página
Origen de la API
Método
Content-Type
Sin cuerpo
Cabecera de petición extra (vacío para ninguna)
Enviar cookies (credentials: include)
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials: true
Veredicto
Mismo origen: CORS no se aplica, la respuesta es legible.
Sin preflight: es una petición simple.
Hace falta preflight: el navegador envía OPTIONS primero.
Motivo: el método no es GET, HEAD ni POST.
Motivo: la cabecera {v} no está en la lista segura.
Motivo: el Content-Type {v} no es uno de los tres tipos simples.