CORS (Cross-Origin Resource Sharing): guía práctica con ejemplos y soluciones reales

Autor: Jeffry Chaves, Ing. en Sistemas – Diccionario Informático

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 o Authorization)
    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.example

  • Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE

  • Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With

  • Access-Control-Max-Age: 600 (reduce preflights repetidos durante 10 min)

  • Sin cuerpo. Status 204 o 200.

Nota: Access-Control-Allow-Credentials: true invalida Access-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:

  1. Network → Request/Response headers: ¿hay Access-Control-Allow-Origin? ¿coincide exactamente con tu front?

  2. ¿La preflight OPTIONS recibe 2xx y los Allow-Methods/Headers que realmente vas a usar?

  3. ¿Estás enviando cookies? → Necesitas credentials: 'include' en fetch y Access-Control-Allow-Credentials: true y un origen específico en Allow-Origin.

  4. ¿Hay CDN/proxy? Añade Vary: Origin y verifica que no estén eliminando headers.

  5. 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):

# Preflight simulado
curl -i -X OPTIONS https://api.example.com/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Authorization, Content-Type"
# Petición real con Origin
curl -i https://api.example.com/users \
-H «Origin: https://app.example.com» \
-H «Authorization: Bearer abc» \
-H «Content-Type: application/json»

5) CORS con credenciales (cookies, Authorization) sin romper seguridad

Para enviar cookies o cabeceras sensibles:

En el servidor

  • Access-Control-Allow-Credentials: true

  • Access-Control-Allow-Origin: https://tu-front.example (no *)

  • Cookies con SameSite=None; Secure si son de terceros (cross-site).

En el cliente

fetch("https://api.example.com/profile", {
credentials: "include", // o axios.withCredentials = true
headers: { "Authorization": "Bearer ..." }
});

Errores típicos

  • * con credenciales → el navegador ignora la respuesta.

  • Cookie sin SameSite=None; Secure → no viaja en contextos cross-site.

  • Falta Vary: Origin y 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:

import express from "express";
import cors from "cors";
const app = express();const allowlist = [«https://app.example.com», «https://admin.example.com»];
const corsOptions = {
origin: (origin, cb) => {
if (!origin || allowlist.includes(origin)) return cb(null, true);
return cb(new Error(«Origin not allowed by CORS»));
},
credentials: true,
allowedHeaders: [«Authorization», «Content-Type»],
methods: [«GET»,«POST»,«PUT»,«PATCH»,«DELETE»,«OPTIONS»],
maxAge: 600,
};
app.use(cors(corsOptions));

// Responder preflights manualmente si quieres control fino:
app.options(«*», cors(corsOptions));

app.get(«/health», (_req, res) => res.json({ ok: true }));
app.listen(3000);

Tips: valida orígenes dinámicos; añade Vary: Origin si usas proxies/CDN.

NGINX (reverse proxy / static)

# En el bloque server o location que proxy-pasa a tu backend
set $cors_origin "";
if ($http_origin ~* ^https?://(app\.example\.com|admin\.example\.com)$) {
set $cors_origin $http_origin;
}# Preflight
location / {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods «GET,POST,PUT,PATCH,DELETE,OPTIONS» always;
add_header Access-Control-Allow-Headers «Authorization,Content-Type» always;
add_header Access-Control-Max-Age 600 always;
add_header Vary «Origin» always;
return 204;
}

proxy_pass http://backend;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials «true» always;
add_header Vary «Origin» always;
}

Spring Boot (Java)

@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://app.example.com","https://admin.example.com")
.allowedMethods("GET","POST","PUT","PATCH","DELETE","OPTIONS")
.allowedHeaders("Authorization","Content-Type")
.allowCredentials(true)
.maxAge(600);
}
};
}
}

Django (+ django-cors-headers)

# settings.py
INSTALLED_APPS = [
...,
"corsheaders",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
...,
]
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = ["authorization","content-type"]
CORS_ALLOW_METHODS = ["GET","POST","PUT","PATCH","DELETE","OPTIONS"]
CORS_PREFLIGHT_MAX_AGE = 600

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:

    // vite.config.js
    export default {
    server: {
    proxy: {
    "/api": {
    target: "https://api.example.com",
    changeOrigin: true,
    },
    },
    },
    };
  • 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: Origin y revisa reglas que pudieran eliminar cabeceras Access-Control-*.

  • Prueba con curl y 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-Origin coincide con tu(s) front(s)?

  • ¿Respondes OPTIONS con 2xx y los Allow-Methods/Headers correctos?

  • ¿Usas cookies/Authorization? → Allow-Credentials: true + origen específico + SameSite=None; Secure.

  • ¿Añadiste Vary: Origin si hay CDN/proxies?

  • ¿Max-Age establecido 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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *