CORS sin misterios: errores comunes y soluciones para Express, Spring Boot y Nginx
Envías un nuevo frontend, abres la consola y aparece un error CORS en rojo: la solicitud fue “bloqueada por la política CORS”. Tu API funciona en Apidog o curl, endcta %}
El hecho fundamental que muchos tutoriales omiten es este: CORS lo aplica el navegador, pero normalmente lo causa el servidor. El navegador bloquea la respuesta porque el servidor no envió los encabezados Access-Control-Allow-Origin correctos. Por eso, la solución casi siempre está en la configuración del servidor y no en el frontend.
En esta guía aprenderás:
- Qué es CORS y cómo funciona.
- Cómo opera una solicitud preflight.
- Cómo resolver los seis errores CORS más comunes.
- Configuraciones funcionales para Express, Spring Boot y Nginx.
- Cómo depurar CORS fuera del navegador.
Qué es un error CORS —y qué no es—
CORS significa Cross-Origin Resource Sharing (intercambio de recursos de origen cruzado). Por defecto, los navegadores aplican la política del mismo origen: un script que se ejecuta en https://app.example.com no puede leer respuestas de https://api.example.com porque el esquema, el host o el puerto son diferentes.
CORS permite que un servidor relaje esta regla de forma explícita. Consulta la documentación de CORS de MDN y la especificación Fetch para conocer todos los detalles.
Tres ideas eliminan la mayor parte de la confusión:
-
El navegador lo aplica. Solo los navegadores realizan comprobaciones CORS. Las llamadas servidor a servidor,
curly los clientes de API de escritorio lo ignoran. - El servidor lo configura. El navegador decide si permite el acceso basándose en los encabezados de respuesta. Sin esos encabezados, tu JavaScript no puede leer la respuesta.
- La solicitud suele llegar al servidor. En solicitudes simples, el servidor procesa la petición y responde; después, el navegador oculta la respuesta al script. CORS no es un muro de seguridad para tu API: protege a los usuarios de páginas maliciosas que intentan leer datos de otro origen usando sus cookies.
Cuando veas un error CORS, no busques un atajo en el frontend. Lee el mensaje y corrige el encabezado que falta o es incorrecto en el servidor.
Anatomía de una solicitud preflight
Antes de determinadas solicitudes de origen cruzado, el navegador envía una solicitud OPTIONS llamada preflight. Ocurre cuando la solicitud:
- Usa un método distinto de
GET,HEADoPOST. - Incluye encabezados personalizados, como
Authorization. - Usa un tipo de contenido como
application/json.
La solicitud preflight puede verse así:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
El navegador pregunta: “¿Una página de app.example.com puede ejecutar aquí un POST con estos encabezados?”. Una respuesta válida sería:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
Si falta alguna pieza, el navegador cancela la solicitud real antes de enviarla. El endpoint nunca se ejecuta y los registros suelen mostrar únicamente el acceso OPTIONS.
Access-Control-Max-Age indica durante cuánto tiempo el navegador puede almacenar este resultado —86400 segundos en el ejemplo— para omitir futuras solicitudes preflight.
La mitad de la depuración de CORS se reduce a una pregunta: ¿falló la preflight o falló la solicitud real?
Los 6 errores CORS más comunes
Los mensajes del navegador suelen indicar exactamente qué falta. Compara el tuyo con los siguientes casos.
1. Falta el encabezado Access-Control-Allow-Origin
El servidor respondió sin ningún encabezado CORS, así que el navegador bloqueó el acceso.
Configura el servidor para enviar el origen específico o * si se trata de una API pública sin credenciales:
Access-Control-Allow-Origin: https://app.example.com
Una trampa habitual es que las respuestas de error no incluyan encabezados CORS. Por ejemplo, el middleware puede añadirlos a las respuestas 200, pero no a un 500. En ese caso, la consola muestra un error CORS y oculta el error real de la API.
Asegúrate de incluir los encabezados en todas las respuestas, incluidas 401, 403 Forbidden y 500. Consulta también esta guía sobre 403 Forbidden.
2. El comodín * no se puede usar con credenciales
El navegador puede mostrar un mensaje similar a:
El valor de
Access-Control-Allow-Originno debe ser*cuando el modo de credenciales esinclude.
Tu frontend envía cookies o credenciales:
fetch(url, { credentials: 'include' })
Pero el servidor responde con:
Access-Control-Allow-Origin: *
La especificación Fetch prohíbe esta combinación. Si cualquier origen pudiera leer respuestas autenticadas, las cookies del usuario quedarían expuestas.
Usa el origen exacto y habilita credenciales:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Valida siempre el encabezado Origin contra una lista blanca antes de reflejarlo. Reflejar orígenes arbitrarios con credenciales habilitadas elimina la protección.
3. La respuesta a la preflight no supera la comprobación
El servidor no gestiona correctamente OPTIONS. Por ejemplo:
- La ruta solo define
POST, por lo queOPTIONSdevuelve404o405. - El middleware de autenticación rechaza la preflight con
401. - La solicitud preflight no contiene el token de autenticación; los navegadores no adjuntan credenciales a estas solicitudes.
Gestiona OPTIONS explícitamente y responde con un estado 2xx antes de ejecutar la autenticación. En la mayoría de los frameworks basta con montar el middleware CORS primero.
Si lo haces manualmente en Express:
app.options('/v1/orders', (req, res) => {
res.set({
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
});
res.sendStatus(204);
});
4. El origen permitido no coincide con el origen enviado
El servidor sí envía Access-Control-Allow-Origin, pero contiene un origen diferente. Las causas más frecuentes son:
- Producción está codificado, pero pruebas desde
http://localhost:5173. - La lista blanca distingue incorrectamente entre
httpyhttps. - Se añadió una barra final.
https://app.example.com/no es un origen válido equivalente ahttps://app.example.com.
Compara el encabezado Origin exactamente con tu lista blanca y refleja solo las coincidencias:
const allowed = [
'https://app.example.com',
'http://localhost:5173'
];
if (allowed.includes(req.headers.origin)) {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Vary', 'Origin');
}
Vary: Origin evita que una caché o CDN entregue a un origen el encabezado generado para otro.
5. El método o encabezado solicitado no está permitido
Son dos variantes del mismo problema:
-
authorizationno está permitido porAccess-Control-Allow-Headers. -
PUTno está permitido porAccess-Control-Allow-Methods.
La preflight funcionó, pero su respuesta no cubre todo lo que necesita la solicitud real. Amplía la configuración:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Los nombres de los encabezados no distinguen mayúsculas y minúsculas. Los métodos sí distinguen mayúsculas y minúsculas, por lo que deben escribirse en mayúsculas.
6. La redirección no está permitida durante una preflight
La solicitud OPTIONS recibió una respuesta 301 o 302. Los navegadores no siguen redirecciones durante una preflight.
Causas habituales:
- Una URL
httpredirige ahttps. - Falta una barra final y el framework redirige automáticamente.
- La puerta de enlace redirige
/v1/ordersa/v1/orders/.
Solución:
- Apunta el frontend directamente a la URL final.
- Usa
httpsdesde el principio. - Respeta la convención de barras finales del router.
- Verifica con una solicitud
OPTIONSmanual que el endpoint devuelva2xx, no3xx.
Ejemplos de configuración del servidor
Express
Usa el middleware oficial de cors en lugar de crear los encabezados manualmente:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400
}));
Monta el middleware antes del middleware de autenticación para que las preflights no sean rechazadas por la falta de tokens.
En Python puedes aplicar el mismo patrón con la extensión Flask-CORS.
Spring Boot
Configura CORS globalmente mediante WebMvcConfigurer:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/v1/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(86400);
}
}
Si usas Spring Security, llama también a .cors(Customizer.withDefaults()) en la cadena de filtros. De lo contrario, la capa de seguridad puede bloquear las preflights antes de que lleguen a la configuración MVC.
Consulta la documentación de Spring CORS para conocer todas las opciones.
Nginx
Si Nginx termina las solicitudes delante de tu aplicación, puedes responder las preflights en el borde:
location /v1/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}
La bandera always es importante: sin ella, Nginx omite add_header en respuestas 4xx y 5xx, lo que reproduce el primer error en cada solicitud fallida.
Elige una sola capa para gestionar CORS. Si Nginx y la aplicación añaden encabezados al mismo tiempo, el navegador puede recibir valores duplicados, como:
Access-Control-Allow-Origin: *, *
En ese caso, rechazará la respuesta.
Depura CORS fuera del navegador con Apidog
El error de la consola indica que el navegador bloqueó algo, pero no muestra con claridad qué envió el servidor. La forma más rápida de comprobarlo es sacar el navegador del circuito.
Apidog es un cliente API de escritorio, por lo que sus solicitudes no están sujetas a las comprobaciones CORS del navegador. Envía desde Apidog la misma solicitud que falló en el frontend:
- Si funciona, la lógica de la API está bien y el problema está en los encabezados CORS.
- Si también falla, tienes un error normal de la API que se estaba interpretando como un problema CORS.
Sigue este flujo:
-
Reproduce la solicitud real. Copia la solicitud fallida desde la pestaña Red del navegador y recréala en Apidog con el mismo método, encabezados y cuerpo. Comprueba el estado y el cuerpo de la respuesta. Un
500significa que CORS probablemente no era el problema. -
Prueba la preflight manualmente. Crea una solicitud
OPTIONSy añade los encabezados que enviaría el navegador:
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
-
Inspecciona la respuesta. Busca
Access-Control-Allow-Origin,Access-Control-Allow-MethodsyAccess-Control-Allow-Headers. Compara cada valor con lo que requiere el frontend. -
Verifica la corrección. Después de cambiar el servidor, reenvía la misma solicitud
OPTIONSguardada y confirma que devuelve los encabezados correctos.
Este procedimiento resuelve rápidamente el clásico “funciona en mi cliente API, pero falla en el navegador”, incluido el caso de la prueba CORS de Postman.
Puedes mantener la solicitud OPTIONS guardada junto a tus pruebas habituales. Para profundizar en las pruebas de API, consulta estas técnicas generales de prueba o descarga Apidog.
Lista de verificación CORS de 30 segundos
Antes de registrar un incidente, revisa:
- ¿La respuesta incluye
Access-Control-Allow-Origin? - ¿Su valor coincide exactamente con el origen de la página: esquema, host y puerto, sin barra final?
- ¿Usas cookies o autenticación? Confirma un origen específico y
Access-Control-Allow-Credentials: true; nunca*. - ¿
OPTIONSdevuelve2xxcon los métodos y encabezados necesarios? - ¿Existe alguna redirección en la URL de la preflight?
- ¿Las respuestas
401,403y500incluyen los mismos encabezados CORS que las respuestas exitosas?
Nueve de cada diez veces, la causa está en una de estas comprobaciones. Confírmalo con una solicitud OPTIONS manual, corrige la configuración del servidor y vuelve a probar.
Preguntas frecuentes
¿Por qué solo obtengo un error CORS en el navegador?
Porque solo los navegadores aplican CORS. La política del mismo origen protege a los usuarios de páginas maliciosas que intentan leer sus datos autenticados. Por eso, el navegador verifica Access-Control-Allow-Origin en cada respuesta de origen cruzado.
curl, los servicios backend y los clientes de escritorio no aplican esta política. Si la solicitud funciona en todas partes excepto en el navegador, normalmente faltan encabezados CORS o están mal configurados; la API puede seguir funcionando correctamente.
¿CORS se aplica a Postman o Apidog?
No. Postman y Apidog son aplicaciones de escritorio, no páginas web ejecutándose en el entorno aislado del navegador, así que omiten CORS.
Esto los hace útiles para la depuración: muestran los encabezados crudos del servidor sin el filtrado del navegador. Una solicitud exitosa en un cliente de escritorio no demuestra que el navegador funcionará, pero sí ayuda a aislar la capa que falla.
¿Un error CORS es una característica de seguridad o un error?
Es una característica de seguridad del navegador. El navegador se niega a exponer datos de respuesta de origen cruzado a los scripts a menos que el servidor lo permita explícitamente.
Deshabilitar CORS mediante flags o extensiones solo oculta el síntoma en tu máquina. Los demás usuarios seguirán viendo el problema. Corrige los encabezados del servidor.
¿Puedo usar Access-Control-Allow-Origin: * en todas partes?
Solo en APIs públicas de solo lectura que no usan cookies ni autenticación.
El comodín se rechaza cuando se incluyen credenciales y anuncia que cualquier origen web puede acceder a la respuesta. Para APIs autenticadas, utiliza una lista blanca de orígenes, refleja únicamente el origen coincidente y envía Vary: Origin para mantener separadas las respuestas almacenadas en caché.
Top comments (0)