DEV Community

Cover image for Cómo solucionar errores CORS: Depuración de Access-Control-Allow-Origin
Roobia
Roobia

Posted on Originally published at apidog.com

Cómo solucionar errores CORS: Depuración de Access-Control-Allow-Origin

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, curl y 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, HEAD o POST.
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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-Origin no debe ser * cuando el modo de credenciales es include.

Tu frontend envía cookies o credenciales:

fetch(url, { credentials: 'include' })
Enter fullscreen mode Exit fullscreen mode

Pero el servidor responde con:

Access-Control-Allow-Origin: *
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 que OPTIONS devuelve 404 o 405.
  • 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);
});
Enter fullscreen mode Exit fullscreen mode

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 http y https.
  • Se añadió una barra final. https://app.example.com/ no es un origen válido equivalente a https://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');
}
Enter fullscreen mode Exit fullscreen mode

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:

  • authorization no está permitido por Access-Control-Allow-Headers.
  • PUT no está permitido por Access-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
Enter fullscreen mode Exit fullscreen mode

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 http redirige a https.
  • Falta una barra final y el framework redirige automáticamente.
  • La puerta de enlace redirige /v1/orders a /v1/orders/.

Solución:

  1. Apunta el frontend directamente a la URL final.
  2. Usa https desde el principio.
  3. Respeta la convención de barras finales del router.
  4. Verifica con una solicitud OPTIONS manual que el endpoint devuelva 2xx, no 3xx.

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
}));
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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: *, *
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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 500 significa que CORS probablemente no era el problema.
  2. Prueba la preflight manualmente. Crea una solicitud OPTIONS y 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
Enter fullscreen mode Exit fullscreen mode
  1. Inspecciona la respuesta. Busca Access-Control-Allow-Origin, Access-Control-Allow-Methods y Access-Control-Allow-Headers. Compara cada valor con lo que requiere el frontend.
  2. Verifica la corrección. Después de cambiar el servidor, reenvía la misma solicitud OPTIONS guardada 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 *.
  • ¿OPTIONS devuelve 2xx con los métodos y encabezados necesarios?
  • ¿Existe alguna redirección en la URL de la preflight?
  • ¿Las respuestas 401, 403 y 500 incluyen 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)