CORS no es un “error del front”, es el navegador aplicando la política de mismo origen. Cuando una web en https://A quiere pedir recursos a https://B, el navegador solo entregará la respuesta al JS de A si B devuelve las cabeceras CORS correctas. Nuestra misión: configurar el servidor (o proxy/CDN) de B para que el navegador confíe.
1) Qué es CORS y por qué existe
CORS es un protocolo de cabeceras HTTP que abre excepciones seguras a la política de mismo origen. Protege a los usuarios de que otras webs roben datos de su sesión sin permiso.
Idea clave: el bloqueo lo hace el navegador, no tu backend. Tu API puede responder 200 OK, pero el navegador oculta esa respuesta a tu JS si las cabeceras no cuadran.
Conceptos base
Origen = protocolo + host + puerto (los tres cuentan).
Permitir = enviar cabeceras
Access-Control-*correctas en el servidor.Diagnóstico: el mensaje real está en la consola del navegador (pestaña Network + detalle de la request).
2) Cómo funciona CORS: simple vs preflight (con OPTIONS)
Hay dos tipos de peticiones:
a) Simple requests (no hay preflight)
Usan métodos “seguros” (GET, HEAD, POST con Content-Type “simple” como application/x-www-form-urlencoded, text/plain, multipart/form-data) y headers limitados.
b) Preflighted requests (sí hay preflight)
Si usas PUT, DELETE, PATCH, Authorization, Content-Type: application/json o headers personalizados, el navegador primero manda un OPTIONS para preguntar:
¿Qué métodos permites?
Access-Control-Allow-Methods¿Qué headers aceptas?
Access-Control-Allow-Headers¿Puedo cachear la decisión?
Access-Control-Max-Age
Si el servidor/proxy no responde al OPTIONS con 200/204 y las cabeceras correctas, la petición real nunca se envía.
3) Cabeceras CORS imprescindibles (y cómo configurarlas bien)
Respuesta de la API (mínimo):
Access-Control-Allow-Origin: https://tu-front.example(evita*si hay cookies/credenciales)Vary: Origin(crítico con CDN/cache si permites varios orígenes)Opcional según caso:
Access-Control-Allow-Credentials: true(si usas cookies oAuthorization)Access-Control-Expose-Headers: X-Total-Count, Link(si el front debe leer headers)
Respuesta al preflight (OPTIONS):
Access-Control-Allow-Origin: https://tu-front.exampleAccess-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETEAccess-Control-Allow-Headers: Authorization, Content-Type, X-Requested-WithAccess-Control-Max-Age: 600(reduce preflights repetidos durante 10 min)Sin cuerpo. Status 204 o 200.
Nota:
Access-Control-Allow-Credentials: trueinvalidaAccess-Control-Allow-Origin: *. Hay que fijar un origen explícito.
4) Errores CORS más comunes y cómo depurarlos
Mensaje típico: “has been blocked by CORS policy”
Pasos rápidos:
Network → Request/Response headers: ¿hay
Access-Control-Allow-Origin? ¿coincide exactamente con tu front?¿La preflight OPTIONS recibe 2xx y los
Allow-Methods/Headersque realmente vas a usar?¿Estás enviando cookies? → Necesitas
credentials: 'include'en fetch yAccess-Control-Allow-Credentials: truey un origen específico enAllow-Origin.¿Hay CDN/proxy? Añade
Vary: Originy verifica que no estén eliminando headers.Postman funciona pero el navegador falla → Postman no aplica CORS. El problema es de cabeceras en la respuesta al navegador.
Prueba de aislamiento con curl (simula preflight y request real):
5) CORS con credenciales (cookies, Authorization) sin romper seguridad
Para enviar cookies o cabeceras sensibles:
En el servidor
Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin: https://tu-front.example(no*)Cookies con
SameSite=None; Securesi son de terceros (cross-site).
En el cliente
Errores típicos
*con credenciales → el navegador ignora la respuesta.Cookie sin
SameSite=None; Secure→ no viaja en contextos cross-site.Falta
Vary: Originy el CDN te sirve la versión para otro origen → fallos intermitentes.
6) Configuración paso a paso por stack (Express, NGINX, Spring, Django)
Express (Node.js)
Opción rápida con cors:
Tips: valida orígenes dinámicos; añade Vary: Origin si usas proxies/CDN.
NGINX (reverse proxy / static)
Spring Boot (Java)
Django (+ django-cors-headers)
7) CORS en desarrollo vs producción: proxies, CDN y Vary: Origin
Desarrollo (DX)
Usa el proxy del dev server (Vite/webpack) para evitar CORS durante el desarrollo:
Alternativa: mock o BFF (Backend For Frontend) local que hable con la API.
Producción
Configura cabeceras en el backend o el reverse proxy (NGINX/Ingress).
Si hay CDN (CloudFront, etc.), añade
Vary: Originy revisa reglas que pudieran eliminar cabecerasAccess-Control-*.Prueba con
curly con navegador; Postman no sirve para verificar CORS.
8) Buenas prácticas para reducir preflights y mejorar performance
Agrupa llamadas y usa métodos idempotentes cuando sea posible.
Prefiere headers simples; evita enviar headers personalizados innecesarios.
Usa
Access-Control-Max-Age(ej. 600–3600) para cachear la política.Sirve recursos estáticos desde el mismo dominio cuando sea posible (fuentes, imágenes) para evitar CORS por diseño.
Si necesitas múltiples orígenes, lista blanca +
Vary: Origin(no*con credenciales).
9) Checklist de despliegue CORS (para no fallar el go-live)
¿
Allow-Origincoincide con tu(s) front(s)?¿Respondes OPTIONS con 2xx y los
Allow-Methods/Headerscorrectos?¿Usas cookies/Authorization? →
Allow-Credentials: true+ origen específico +SameSite=None; Secure.¿Añadiste
Vary: Originsi hay CDN/proxies?¿
Max-Ageestablecido para reducir preflights?¿Probaste con
curl+ navegador (no solo Postman)?
Conclusión
CORS es sencillo cuando aceptas su regla de oro: el front no “arregla” CORS; lo hace el servidor (o proxy/CDN) con cabeceras correctas. Con los snippets de arriba, un par de curl bien puestos y el Vary: Origin en producción, deberías pasar de “bloqueado por CORS” a “todo verde” sin dramas.
FAQs
¿Por qué en Postman funciona y en el navegador no?
Porque Postman no aplica CORS; el bloqueo es del navegador.
¿Puedo usar * con Access-Control-Allow-Credentials: true?
No. Si hay credenciales, debes fijar un origen exacto.
¿Qué dispara el preflight?
Métodos no simples (PUT/DELETE/PATCH), Content-Type: application/json y headers personalizados como Authorization.
¿Cómo reduzco preflights?
Evita headers no esenciales, cachea la política con Max-Age, consolida llamadas.