W3docs

JavaScript Fetch: Solicitudes de origen cruzado (CORS)

Aprende a realizar solicitudes de origen cruzado con la Fetch API de JavaScript: cómo funciona CORS, las opciones mode y credentials, solicitudes preflight y buenas prácticas para gestionar errores.

Las solicitudes de origen cruzado permiten que una página web cargue datos desde un servidor en un origen diferente al de la propia página. Son el motor de casi todas las aplicaciones modernas: llamar a una API pública, comunicarse con tu propio backend en otro subdominio o integrar un servicio de terceros. Esta página explica cómo funcionan las reglas CORS del navegador, cómo realizar llamadas de origen cruzado con la Fetch API y cómo gestionar correctamente las credenciales, las solicitudes preflight y los errores.

Qué se considera "origen cruzado"

Un origen es la combinación de protocolo + host + puerto. Dos URL comparten el mismo origen solo cuando los tres coinciden. Si alguno difiere, la solicitud entre ellas es de origen cruzado.

Solicitud desde https://app.example.com hacia…¿Mismo origen?Por qué
https://app.example.com/api/usersprotocolo, host y puerto idénticos
http://app.example.com/apiNoprotocolo diferente (http vs https)
https://api.example.com/usersNohost diferente (el subdominio cuenta)
https://app.example.com:8443/apiNopuerto diferente

Las solicitudes del mismo origen no tienen restricciones. Las solicitudes de origen cruzado se rigen por CORS.

Qué es CORS (y qué no es)

Cross-Origin Resource Sharing (CORS) es un mecanismo de seguridad del navegador que decide si JavaScript en un origen tiene permiso para leer una respuesta de otro origen. La decisión la toma el servidor, que la habilita enviando cabeceras de respuesta HTTP específicas. El navegador las aplica.

Dos cosas que se confunden con facilidad:

  • CORS no es algo que se solucione únicamente en el código del frontend. Si el servidor no envía las cabeceras correctas, ninguna opción de fetch conseguirá que la lectura tenga éxito.
  • La solicitud a menudo llega igualmente al servidor y se ejecuta allí; CORS solo impide que tu script lea la respuesta. Por eso CORS no sustituye a la autenticación.

Realizar una solicitud de origen cruzado con Fetch

La Fetch API es la forma moderna y basada en promesas de realizar solicitudes HTTP. Una llamada sencilla a fetch dirigida a otro origen ya es de origen cruzado: el navegador gestiona CORS de forma automática.

javascript— editable

Esto funciona porque jsonplaceholder.typicode.com devuelve Access-Control-Allow-Origin: *. Si no lo hiciera, el navegador bloquearía la lectura y fetch rechazaría la promesa. Consulta el capítulo sobre la Fetch API para conocer el modelo completo de solicitud/respuesta.

Las cabeceras del servidor que lo hacen posible

Cuando realizas una solicitud de origen cruzado, el servidor debe incluir cabeceras CORS para permitirla. Las que encontrarás con más frecuencia:

  • Access-Control-Allow-Origin — qué origen(es) pueden leer la respuesta (un origen concreto o * para cualquiera).
  • Access-Control-Allow-Methods — qué métodos HTTP están permitidos (se usa en las respuestas preflight).
  • Access-Control-Allow-Headers — qué cabeceras de solicitud puede enviar el cliente (se usa en las respuestas preflight).
  • Access-Control-Allow-Credentials — si se pueden enviar cookies o credenciales de autenticación (debe ser true para permitir credenciales).

Estas cabeceras se configuran en el servidor, no en fetch. El frontend solo controla la solicitud.

La opción mode

La opción mode de Fetch declara cómo debe comportarse el manejo de solicitudes de origen cruzado:

  • 'cors' — el valor predeterminado. La solicitud solo se permite si el servidor devuelve las cabeceras CORS correspondientes; de lo contrario, la lectura queda bloqueada.
  • 'same-origin' — falla de inmediato ante cualquier URL de origen cruzado.
  • 'no-cors' — envía la solicitud pero devuelve una respuesta opaca: no puedes leer su estado, cabeceras ni cuerpo. Útil solo para casos de disparar y olvidar, como almacenar en caché una imagen en un service worker.

Dado que 'cors' ya es el valor predeterminado, rara vez necesitarás establecerlo, aunque hacerlo de forma explícita documenta la intención:

Advertencia

En el servidor, evita Access-Control-Allow-Origin: * en producción. Permite únicamente los orígenes específicos en los que confíes.

javascript— editable

Envío de credenciales

Por defecto, las solicitudes fetch de origen cruzado no envían cookies ni cabeceras de autenticación HTTP. Para incluirlas, establece la opción credentials en 'include'. El servidor también debe responder con Access-Control-Allow-Credentials: true y especificar tu origen exacto en Access-Control-Allow-Origin; el valor * se rechaza cuando hay credenciales de por medio. Consulta Cookies y document.cookie para entender cómo se comportan las cookies entre sitios.

async function fetchWithCredentials(url) {
  try {
    const response = await fetch(url, {
      mode: 'cors',
      credentials: 'include'
    });
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

fetchWithCredentials('https://api.crossorigin.com/secure-data');
Nota

Cuando una política CORS bloquea una lectura de origen cruzado, el navegador no entrega la respuesta a tu script. En su lugar, fetch rechaza la promesa con un TypeError ("Failed to fetch"), por lo que el error llega a tu bloque catch, no a la comprobación if (!response.ok).

Solicitudes preflight

Para las solicitudes que el navegador considera potencialmente inseguras, primero envía automáticamente una solicitud OPTIONS —una preflight— para preguntar al servidor si se permite la solicitud real. Tu código nunca emite esta llamada OPTIONS; el navegador lo hace por ti.

Una solicitud activa una preflight cuando no es una "solicitud simple", es decir, cuando:

  • utiliza un método distinto de GET, HEAD o POST;
  • incluye cabeceras de solicitud personalizadas (por ejemplo Authorization o X-Api-Key);
  • usa un Content-Type distinto de application/x-www-form-urlencoded, multipart/form-data o text/plain (enviar JSON con application/json es el desencadenante más habitual).

Un POST JSON típico supone, por tanto, dos viajes de ida y vuelta: la preflight y luego la solicitud real:

async function createPost(url, payload) {
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }, // triggers a preflight
      body: JSON.stringify(payload)
    });
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

createPost('https://api.example.com/posts', { title: 'Hello' });

Para que la preflight sea exitosa, el servidor debe responder a la solicitud OPTIONS con Access-Control-Allow-Methods y Access-Control-Allow-Headers que cubran lo que utilizará la solicitud real. Si falla, el navegador genera un error de red y la solicitud real nunca se envía.

Distinguir los modos de fallo

Los errores CORS son notoriamente confusos porque el navegador oculta los detalles por seguridad. Usa esta lista de comprobación:

  • fetch rechaza la promesa ("Failed to fetch") — generalmente un bloqueo CORS, un fallo de red o una preflight fallida. Revisa la consola del navegador para ver el mensaje CORS específico; no está expuesto a JavaScript.
  • response.ok es false — la solicitud tuvo éxito y CORS no la bloqueó, pero el servidor devolvió un estado 4xx/5xx. Se trata de un error de aplicación, no de CORS.
  • Las cookies no se envían — olvidaste credentials: 'include', o el servidor no devuelve Access-Control-Allow-Credentials: true con un origen específico.

Envuelve las llamadas en try/catch e inspecciona response.ok por separado, tal como hacen los ejemplos anteriores.

Buenas prácticas

  • Restringe Access-Control-Allow-Origin. Lista los orígenes exactos en los que confías en lugar de usar *, especialmente cuando hay credenciales de por medio (* se rechaza con credenciales).
  • Usa siempre HTTPS. Protege los datos en tránsito y es obligatorio para muchas API de contexto seguro.
  • Reduce las preflights. Evita cabeceras personalizadas innecesarias y deja que el servidor envíe Access-Control-Max-Age para que el navegador almacene en caché los resultados de las preflights.
  • Gestiona los errores con elegancia. Separa los fallos de transporte/CORS (catch) de los errores de estado HTTP (!response.ok) y muestra mensajes útiles al usuario.
  • No confíes en CORS para la seguridad. Controla lo que los navegadores permiten leer a los scripts; no autentica al llamante. Valida y autoriza cada solicitud en el servidor.

Capítulos relacionados

Resumen

Una solicitud es de origen cruzado cuando su protocolo, host o puerto difiere del de la página. CORS permite que el servidor decida qué orígenes pueden leer la respuesta, y el navegador aplica esa decisión. Con Fetch, el modo 'cors' es el predeterminado; usa la opción credentials para enviar cookies, prevé una solicitud preflight OPTIONS para las llamadas no simples y gestiona los fallos de CORS en catch mientras compruebas response.ok para los errores a nivel HTTP.

Was this page helpful?