← El cliente del proyecto: navegador y CORS

Sesión 33 · Semana 17

Construir el primer cliente y diagnosticar CORS

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado revisar y publicar un contrato compatible. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

El portfolio de Intermodular presenta tu producto, pero todavía no necesita consumir su API. Hoy crearás esa conexión desde una página mínima. CORS es el mecanismo por el que el servidor indica qué otros orígenes pueden leer sus respuestas; un origen combina protocolo, host y puerto.

El navegador como entorno hostil y seguro

Durante todo el primer trimestre has probado tu API utilizando herramientas como Bruno o Postman. Esas herramientas son programas de escritorio independientes:

  • Se ejecutan como procesos nativos en tu sistema operativo.
  • No están atados a ninguna pestaña de navegación.
  • No guardan cookies compartidas de usuarios ni sesiones de otros sitios web.
  • Hacen exactamente lo que tú les ordenas, sin restricciones de origen.

El navegador web, en cambio, es un entorno completamente distinto:

  • Ejecuta código JavaScript descargado de servidores de terceros en una caja de arena (Sandbox).
  • Administra sesiones activas, cookies de autenticación, contraseñas y datos privados del usuario.
  • Debe proteger al usuario para que un script malicioso descargado de una web desconocida no pueda usar sus credenciales para consultar datos privados en otro servidor en segundo plano.

La primera regla de integración

Una API que funciona en Postman no ha demostrado todavía que pueda usarse en la web.

Hasta que tu servidor no responda a una petición emitida por un motor de JavaScript real dentro de un navegador, la integración de la API no está verificada.

La API Fetch nativa: asincronía y procesamiento en dos fases

En JavaScript moderno no se necesitan librerías externas (como Axios o jQuery) para hablar con una API REST. Los navegadores incluyen de forma nativa la función fetch().

Consumir un endpoint en el navegador ocurre siempre en dos fases asíncronas:

Las dos fases de una petición con fetch()
  1. 1. fetch(url) → Espera cabeceras de red
  2. 2. response.ok / response.status
  3. 3. response.json() → Espera descarga y parseo de bytes
  4. 4. Renderizado en el DOM
async function cargarProyectos() {
  try {
    // Fase 1: Enviamos la petición y esperamos las cabeceras HTTP del servidor
    const response = await fetch('http://localhost:8080/api/v1/proyectos');

    // Verificamos si el servidor respondió con un código 2xx
    if (!response.ok) {
      throw new Error(`Error del servidor: HTTP ${response.status}`);
    }

    // Fase 2: Descargamos el cuerpo completo de la respuesta y lo parseamos como JSON
    const data = await response.json();

    // Accedemos al array (si la API es paginada vendrá en data.content)
    const proyectos = data.content || data;
    renderizarProyectos(proyectos);

  } catch (error) {
    console.error('Fallo en la comunicación con la API:', error);
  }
}

fetch() no falla ante un 404

fetch() solo rechaza la promesa (entra en el bloque catch) si ocurre un fallo catastrófico de red (cable desconectado, DNS fallido o servidor completamente apagado). Si el servidor responde con un código de error como 404 Not Found o 500 Internal Server Error, la promesa se resuelve con éxito. Por eso es obligatorio comprobar siempre if (!response.ok).

La Política del Mismo Origen (Same-Origin Policy - SOP)

Para entender CORS, primero hay que entender qué es un Origen. Un origen está formado estrictamente por la combinación de tres elementos:

La anatomía de un origen web
  1. Esquema / Protocolo (http://)
  2. Host / Dominio (localhost)
  3. Puerto (:8080)

Dos URLs pertenecen al mismo origen si y solo si sus tres componentes coinciden exactamente:

URL A URL B ¿Mismo origen? Razón
http://localhost:8080/api/v1 http://localhost:8080/swagger-ui Mismo esquema, mismo host y mismo puerto (8080).
http://localhost:5500 http://localhost:8080 NO (Cross-Origin) Diferente puerto (5500 vs 8080).
http://midominio.com https://midominio.com NO (Cross-Origin) Diferente protocolo (http vs https).
http://app.empresa.com http://api.empresa.com NO (Cross-Origin) Diferente subdominio (app vs api).

Por defecto, la Política del Mismo Origen prohíbe terminantemente que un script descargado de un origen (http://localhost:5500) lea los datos devueltos por otro origen (http://localhost:8080).

¿Por qué Postman no sufre CORS?

Postman, Bruno y los comandos de consola curl son herramientas de prueba para desarrolladores:

  • No tienen usuarios navegando por Internet.
  • No guardan sesiones bancarias ni cookies privadas de terceros en segundo plano.
  • No implementan la Política del Mismo Origen: envían la petición directamente al socket TCP del servidor y leen la respuesta sin restricciones.

El navegador, en cambio, defiende al usuario: si entras en web-sospechosa.com, el navegador impide que el JavaScript de esa página haga un fetch('https://tu-banco.com/saldo') aprovechando tus cookies activas.

Cómo funciona CORS: Peticiones con verificación previa (Preflight OPTIONS)

Para relajar esta restricción de forma segura cuando el frontend y el backend están en servidores separados, el navegador y el servidor dialogan mediante cabeceras HTTP:

  1. Cuando el frontend envía una petición compleja (por ejemplo, un POST con Content-Type: application/json o un PUT/DELETE), el navegador no lanza el POST directamente.
  2. Primero envía automáticamente una petición de sondeo o verificación previa (Preflight Request) con el método HTTP OPTIONS.
  3. El navegador le pregunta al servidor: «Oye, backend, tengo un script en http://localhost:5500 que quiere enviarte un POST con JSON. ¿Me autorizas?».
  4. El servidor responde con las cabeceras de autorización:
    • Access-Control-Allow-Origin: http://localhost:5500
    • Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
    • Access-Control-Allow-Headers: Content-Type, Authorization
  5. Si el servidor aprueba el origen, el navegador ejecuta la petición real. Si el servidor no responde con la cabecera adecuada, el navegador aborta la conexión y tiñe la consola de rojo.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre el repositorio del backend, arráncalo siguiendo su README y comprueba una consulta conocida con Postman o Bruno. Anota la dirección completa y conserva una respuesta de ejemplo.
  2. En la raíz de ese repositorio crea la carpeta cliente. En el siguiente paso guardarás allí la página que consumirá esa consulta; todavía no necesitas un cliente funcionando.
  3. Abre las herramientas de desarrollo del navegador y localiza la pestaña Red. Cuando sirvas la página, compararás su origen con el del backend para decidir qué origen permitir en Spring.

Paso 2 · El primer cliente web (index.html)

Guarda el ejemplo en cliente/index.html dentro del repositorio del backend. Para servir esta carpeta por HTTP, abre una terminal en ella y ejecuta npx http-server . -p 5500 -c-1, con Node.js y npm preparados en Intermodular 3; acepta la instalación del paquete si se solicita. Deja esa terminal abierta y visita http://localhost:5500/index.html. Primero comprueba que carga el HTML; después prueba su botón. El backend debe estar encendido en otra terminal y el listado paginado devuelve sus elementos en content. Si aparece un error de CORS, conserva el mensaje: los siguientes pasos explican cómo resolverlo.

Este es el contenido inicial de cliente/index.html; adapta la ruta y los campos a tu producto:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Gestor de Proyectos · Cliente Web</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
    ul { list-style: none; padding: 0; }
    li { background: #f4f4f5; margin-bottom: 0.5rem; padding: 0.75rem; border-radius: 6px; display: flex; justify-content: space-between; }
    .badge { background: #22c55e; color: white; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
    .badge--inactivo { background: #94a3b8; }
  </style>
</head>
<body>
  <h1>Proyectos en curso</h1>
  <button id="btn-cargar">Actualizar lista</button>
  <ul id="lista-proyectos"></ul>

  <script>
    const lista = document.getElementById('lista-proyectos');
    const btn = document.getElementById('btn-cargar');

    async function obtenerProyectos() {
      lista.innerHTML = '<li>Cargando proyectos...</li>';
      try {
        const res = await fetch('http://localhost:8080/api/v1/proyectos');
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = await res.json();
        const proyectos = data.content || data;

        lista.innerHTML = '';
        proyectos.forEach(p => {
          const item = document.createElement('li');
          item.innerHTML = `
            <strong>${p.nombre}</strong>
            <span class="badge ${p.activo ? '' : 'badge--inactivo'}">${p.activo ? 'Activo' : 'Pausado'}</span>
          `;
          lista.appendChild(item);
        });
      } catch (err) {
        lista.innerHTML = `<li style="color: red;">Error al conectar con la API: ${err.message}</li>`;
      }
    }

    btn.addEventListener('click', obtenerProyectos);
    obtenerProyectos(); // Carga automática al abrir
  </script>
</body>
</html>

Para que el navegador se comporte como un cliente web real, no abras el archivo haciendo doble clic (file:///C:/...), ya que ese protocolo desactiva funcionalidades web estándar.

Arranca un servidor estático ligero en la carpeta cliente. Puedes usar cualquiera de estas opciones estándar:

  • Con VS Code: Extensión Live Server (clic derecho en index.htmlOpen with Live Server en http://localhost:5500).
  • Con Python: Ejecuta en la terminal de la carpeta cliente: python -m http.server 5500.
  • Con Node.js: npx serve . -l 5500.
Por qué data.content || data
Porque desde la sesión 30 tu endpoint devuelve una página, no una lista: el array viene dentro de content, junto a los metadatos de paginación. Esa línea acepta las dos formas para que el cliente funcione hayas paginado ya o no. Es un apaño consciente de una página de prueba; en un cliente de verdad, el contrato se fija y no se adivina.
if (!res.ok) throw: la línea que casi todo el mundo olvida
fetch() solo rechaza la promesa ante un fallo de red. Un 404 o un 500 son respuestas perfectamente válidas para fetch, así que sin esa comprobación tu código seguiría adelante e intentaría recorrer un objeto de error como si fuera una lista de proyectos. El síntoma es una página en blanco sin ningún error en la consola.
await dos veces
El primer await espera a que lleguen las cabeceras y el estado. El segundo, el de res.json(), espera a que llegue y se interprete el cuerpo. Son dos esperas porque son dos momentos distintos: el navegador ya sabe el código de estado antes de haber descargado la respuesta entera.

Paso 3 · Inspeccionar peticiones, respuestas y cookies en DevTools

Con tu backend Spring Boot arrancado en el puerto 8080, abre http://localhost:5500 en tu navegador y pulsa F12:

  1. Abre la pestaña Red (Network):

    • Selecciona el filtro Fetch/XHR para ocultar imágenes o estilos y ver solo las peticiones de datos.
    • Haz clic en el botón «Actualizar lista».
  2. Inspecciona la fila de la petición proyectos:

    • Status: Debe marcar 200 OK.
    • Type: Debe indicar fetch.
    • Initiator: Muestra el archivo y línea exacta de JavaScript que ejecutó la llamada (index.html:24).
  3. Analiza las cabeceras:

    • Haz clic en la petición y entra en la pestaña Headers.
    • Revisa en Response Headers que el backend devolvió Content-Type: application/json.
  4. Analiza la pestaña Preview / Response:

    • Comprueba que visualizas el árbol de objetos JSON exactamente como lo programaste en Spring Boot.
  5. Compara con tu cliente HTTP: lanza la misma petición desde Bruno o Postman y pon las dos respuestas una al lado de otra. Son idénticas. Tu backend no se ha enterado de que quien llama es un navegador, y eso es exactamente lo que debe pasar: HTTP es HTTP venga de donde venga.

Paso 4 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
blocked by CORS policy en la consola Es el fallo que esta sesión quiere provocar No lo arregles todavía: es el tema entero de la sesión 33
La barra del navegador dice file:///C:/... Has abierto el HTML con doble clic Tienes que servirlo: sin origen, ni CORS ni fetch se comportan como en la vida real
Failed to fetch y la terminal de Spring en silencio El backend no está escuchando ¿Arrancado? ¿En el 8080? Prueba la URL directamente en otra pestaña
La lista sale vacía sin ningún error La respuesta no tiene la forma esperada Pon console.log(data) justo después del res.json() y mira qué llega de verdad
Cannot read properties of undefined Estás leyendo un campo que el DTO no publica Compara los nombres con los del ProyectoResponse de la UD7, no con los de la entidad
Cambias el HTML y el navegador no se entera Caché del navegador Ctrl+Shift+R, o marca Disable cache en DevTools con las herramientas abiertas

Paso 5 · Visualizar las tareas al seleccionar un proyecto

Añade al elemento de cada proyecto un botón creado con document.createElement('button'); así podrás asociar su evento al id del objeto, sin construir JavaScript dentro de HTML. En el manejador, solicita el subrecurso de ese id, comprueba response.ok, lee su JSON y crea la sublista con textContent. Si CORS aún bloquea la lectura, conserva el código y completa primero su configuración en el paso 7; después repite la prueba.

  1. Modifica la generación de cada elemento de la lista para que incluya un botón «Ver tareas».
  2. Al pulsarlo, lanza una segunda llamada a /api/v1/proyectos/{id}/tareas y renderiza las tareas en una sublista bajo el proyecto.
  3. Inspecciona en DevTools cómo se suceden ambas peticiones en cascada, y fíjate en el Initiator de la segunda: apunta a la línea de tu código que la disparó.
  4. Trata los tres estados de una petición, que es lo que separa una página que funciona de una que parece rota: mientras carga, muestra un texto de espera; si responde bien pero la lista viene vacía, di «este proyecto no tiene tareas» en vez de dejar el hueco en blanco; si falla, muestra el código de estado.
  5. Pide un proyecto que no exista (/api/v1/proyectos/9999/tareas) y comprueba que tu página muestra el 404 en lugar de quedarse pensando. Ese 404 es la regla que implementaste en la sesión 29: ahora la estás viendo desde el otro lado.
  6. Anota en tu cuaderno cuántas peticiones lanza tu página al mostrar cinco proyectos con sus tareas. Con un botón por proyecto son seis, y además bajo demanda. Cargándolas todas de una vez serían seis en cualquier caso. Ese es el mismo N+1 de la UD5, ahora sobre la red.
Cómo saber que lo has terminado
La página lista proyectos y sus tareas contra tu API real; los tres estados —cargando, vacío y error— se ven distintos en pantalla; y has comprobado que la respuesta es idéntica a la que te daba tu cliente HTTP.

CORS: por qué el navegador bloquea lo que Postman no

Paso 6 · Comparar la petición del cliente HTTP con la del navegador

Cualquier desarrollador backend novel pasa por este momento de desesperación:

  1. Crea un endpoint en Spring Boot.
  2. Abre Postman o Bruno, envía la petición y recibe un maravilloso 200 OK.
  3. Abre su página web en el navegador, ejecuta un fetch(), y en la consola de JavaScript aparece un mensaje en rojo aterrador:
Access to fetch at 'http://localhost:8080/api/v1/proyectos' from origin 'http://localhost:5500'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

El programador mira la consola de Spring Boot: no hay ningún error, ninguna traza de excepción. Vuelve a Postman: sigue funcionando. ¿Qué está pasando?

La realidad sobre CORS

CORS no es un error de Spring Boot ni un fallo de programación en JavaScript.

CORS (Cross-Origin Resource Sharing) es un mecanismo de seguridad implementado por el navegador web para proteger a los usuarios frente a peticiones no autorizadas entre sitios distintos.

Paso 7 · Configurar CORS de forma acotada en Spring Boot

Crea config/WebConfig.java solo si aún no existe una configuración MVC para CORS; si existe, modifica su método addCorsMappings. Introduce el origen exacto desde el que has servido el cliente y el prefijo /api/v1/**. Reinicia Java y repite la misma petición del navegador sin cambiar su código. CORS controla qué orígenes pueden leer respuestas en el navegador; los permisos sobre usuarios se incorporarán con Spring Security.

La forma profesional de configurar CORS en Spring Boot es de forma centralizada, explícita y restringida a los orígenes autorizados.

En tu proyecto Spring Boot, crea una clase de configuración que implemente WebMvcConfigurer:

package com.ejemplo.gestor.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Value("${app.cors.allowed-origins:http://localhost:5500,http://127.0.0.1:5500}")
    private String[] allowedOrigins;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins(allowedOrigins)
            .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .exposedHeaders("Location") // Permite al cliente leer la cabecera Location en los 201 Created
            .allowCredentials(true)
            .maxAge(3600); // El navegador cachea la respuesta preflight durante 1 hora (3600s)
    }
}
Qué es exactamente un «origen»
La terna esquema + host + puerto. http://localhost:5500 y http://127.0.0.1:5500 son orígenes distintos aunque apunten a la misma máquina, y http://localhost:5500 y https://localhost:5500 también. Por eso la lista de arriba trae los dos: es el motivo número uno de que «a mí me funciona y a ti no».
Por qué addMapping("/api/**") y no "/**"
Porque solo tu API necesita ser consumida desde otro origen. Abrir la aplicación entera incluiría rutas que no tienen por qué estar expuestas. La regla es la misma que en cualquier permiso: el ámbito más estrecho que haga el trabajo.
exposedHeaders("Location"), la línea que parece sobrar
El navegador solo deja leer a JavaScript un puñado de cabeceras «seguras». Tu Location del 201 Created llega en la respuesta —lo verás en DevTools— pero el cliente no la encuentra si no la declaras aquí. Es un fallo desconcertante porque la petición ha ido bien.
maxAge(3600)
Sin esto, el navegador lanza un OPTIONS extra antes de cada petición: duplicas el número de viajes. Con la caché de preflight, el navegador pregunta una vez por hora. Ojo al depurar: si cambias la configuración y parece que no surte efecto, es esta caché.

Configuramos los orígenes permitidos en el archivo de propiedades para poder adaptarlos según el entorno (desarrollo, pruebas o producción):

# Orígenes web autorizados para interactuar con la API
app.cors.allowed-origins=http://localhost:5500,http://127.0.0.1:5500

Por qué esto no se escribe a fuego en el código

El origen del cliente cambia con el entorno: :5500 en tu portátil, :4200 cuando llegue Angular en la UD12, y un dominio real el día del despliegue. Si la lista está dentro de una clase Java, cada entorno exige recompilar. En application.properties —y mejor aún, como variable de entorno— es configuración, que es lo que es.

Paso 8 · Verificar el Preflight en DevTools

  1. Reinicia tu aplicación Spring Boot.
  2. Vuelve a tu navegador en http://localhost:5500 y pulsa Actualizar lista.
  3. Comprueba que el mensaje rojo de CORS ha desaparecido por completo y los proyectos se renderizan.
  4. Abre la pestaña Network en DevTools y examina la petición:
    • En Response Headers verás la cabecera emitida por Spring Boot: Access-Control-Allow-Origin: http://localhost:5500
  5. Abre la consola de JavaScript: 0 advertencias, 0 errores.

Un GET sencillo no dispara preflight: el navegador lo considera una «petición simple» y va directo. Para verlo hay que provocarlo.

  1. En DevTools → Network, marca la casilla Preserve log y filtra por Fetch/XHR.

  2. Desde tu página, lanza un POST con Content-Type: application/json (el botón de crear proyecto).

  3. Ahora sí aparecen dos líneas para una sola operación:

    # Método Ruta Estado Qué es
    1 OPTIONS /api/v1/proyectos 200 El navegador preguntando «¿me dejas?»
    2 POST /api/v1/proyectos 201 La petición de verdad, ya autorizada
  4. Pincha la primera y busca en Request Headers la cabecera Access-Control-Request-Method: POST, y en Response Headers la respuesta de Spring: Access-Control-Allow-Methods.

  5. Lo importante: ese OPTIONS lo envía el navegador solo. No está programado en el cliente y el controlador no lo atiende. El backend no llegó a ejecutar la lógica del POST hasta que el navegador obtuvo la autorización previa.

Qué convierte una petición en «no simple»

Basta con cualquiera de estas tres cosas: un método distinto de GET, POST o HEAD; un Content-Type que no sea de formulario o texto plano —application/json lo es—; o una cabecera propia como Authorization.

Es decir: en cuanto tu API empiece a recibir JSON o tokens, toda escritura llevará su preflight por delante. Conviene verlo hoy, con una página de veinte líneas, y no en la UD9 con seguridad de por medio.

Paso 9 · Si algo no sale como dice el guion

Mensaje en la consola del navegador Qué significa de verdad Qué mirar
No 'Access-Control-Allow-Origin' header is present Spring respondió, pero sin autorizar tu origen Compara letra a letra el origen de tu página con el de application.properties, puerto incluido
must not be the wildcard '*' when credentials mode is 'include' Comodín más credenciales Enumera los orígenes, o quita allowCredentials(true) si no lo necesitas
El OPTIONS responde 403 La ruta del preflight no está permitida Con Spring Security aún sin instalar esto no debería pasar; si pasa, revisa el addMapping
Cambias la configuración y no surte efecto La caché del preflight maxAge es de una hora: recarga con Ctrl+Shift+R o desmarca la caché en DevTools
Funciona en tu cliente HTTP y falla en el navegador Es CORS, por definición Postman y Bruno no aplican la política de mismo origen: esa asimetría es el diagnóstico
Failed to fetch sin información adicional El servidor no llegó a responder Comprueba que la aplicación está arrancada y que el puerto es el correcto: esto no es CORS

Paso 10 · Consumir y comprobar la API desde el cliente del proyecto

  1. Añade http://localhost:3000 a la lista de orígenes en application.properties.
  2. Reinicia y comprueba que los tres orígenes (5500, 127.0.0.1:5500 y 3000) son aceptados.
  3. Sirve temporalmente la página desde http://localhost:9999 (por ejemplo, python -m http.server 9999 desde cliente), sin añadir ese origen a los permitidos, y observa el bloqueo. Copia el mensaje exacto en tu cuaderno: es el que te vas a encontrar en la UD12 con Angular.
  4. Lee la cabecera Location: haz que tu página, tras crear un proyecto, muestre la URL del recurso recién creado leyendo esa cabecera de la respuesta. Comprueba que puedes leerla. Retira temporalmente exposedHeaders("Location"), reinicia y observa que JavaScript deja de acceder a ella aunque siga visible en Red. Restaura la configuración. Es un fallo que se busca durante horas si no lo has visto antes.
  5. Diagnóstico a tres bandas: por cada uno de estos tres fallos, di si el problema es del cliente, del servidor o del navegador, y cómo lo has sabido:
    • La página muestra la lista vacía y la consola no dice nada.
    • La consola dice blocked by CORS policy pero la pestaña Network muestra que el servidor respondió 200.
    • El servidor responde 404 y la página se queda en blanco.
  6. Escribe en tres líneas, con tus palabras, por qué CORS no protege tu API. Si la respuesta no menciona que cualquiera puede llamarla desde fuera de un navegador, vuelve a leer el primer apartado de la sesión: es la idea que hay que llevarse a la UD9.
Cómo saber que lo has terminado
Has visto el par OPTIONS + POST en DevTools con tus propios ojos; sabes provocar y reconocer un bloqueo de CORS; tu página lee la cabecera Location; y puedes explicar por qué la misma petición pasa desde tu cliente HTTP y no desde el navegador.

Paso 11 · Comprobar y registrar el resultado del proyecto

  1. Repite la misma consulta desde el cliente HTTP y desde el navegador, y distingue respuesta del servidor de bloqueo de lectura por CORS.
  2. Comprueba la petición OPTIONS cuando exista preflight y verifica que un origen permitido funciona y uno no autorizado no recibe permiso de lectura.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Indicador de latencia y simulación de redes lentas

En producción los usuarios no navegan en redes locales a 0 milisegundos de latencia.

Aprende a diagnosticar la experiencia de usuario ante redes degradadas:

  1. En la pestaña Network de DevTools, localiza el selector de Throttling (por defecto en No throttling).
  2. Cámbialo a Slow 3G (3G lenta) y pulsa Actualizar lista.
  3. Observa en la columna Waterfall (cascada) cómo el tiempo de espera (TTFB - Time to First Byte) se dispara a varios segundos.
  4. ¿Por qué una interfaz que no muestra un indicador de carga (Loading spinner o texto «Cargando…») hace que el usuario crea que la aplicación se ha colgado y pulse diez veces seguidas el botón?
Objetivo mínimoPágina index.html consumiendo GET /api/v1/proyectos con fetch nativo servida en servidor local.
Si lo tienesConsulta interactiva del subrecurso de tareas al pulsar sobre un proyecto con renderizado en el DOM.
RetoAuditoría de red con simulación Slow 3G en DevTools y gestión de estados de carga visuales implementada.
Ver respuestas

1 · Porque la promesa de fetch() solo se rechaza ante fallos de red a nivel de transporte (imposibilidad de conectar con el host); un código 404 o 500 es una respuesta HTTP válida recibida del servidor.

2 · El protocolo file:/// carece de origen HTTP válido (origin: null), lo que desactiva mecanismos estándar de seguridad, cookies y políticas de recursos en el navegador.

3 · El desglose cronológico de la conexión: tiempo de resolución DNS, negociación TCP/TLS, tiempo de espera hasta el primer byte del servidor (TTFB) y tiempo de descarga del contenido.

4 · Porque una respuesta paginada de Spring Data encapsula el array de elementos dentro de la propiedad content, acompañada de metadatos como page, size y totalElements.

Reto · La incompatibilidad entre credenciales y comodines

Existe una regla de seguridad estricta en la especificación de CORS del W3C:

  • Si una aplicación backend configura allowCredentials(true) (para admitir cookies o cabeceras de autorización Authorization), el navegador rechaza terminantemente el uso del comodín allowedOrigins("*").

Investiga y responde con criterio técnico:

  1. ¿Qué vulnerabilidad crítica sufrirían los usuarios si un navegador permitiera Access-Control-Allow-Origin: * combinado con el envío de cookies de sesión autenticadas (withCredentials: true)?
  2. ¿Por qué Spring Boot lanza una excepción al arrancar si detectas que has configurado simultáneamente allowedOrigins("*") y allowCredentials(true)?

Formato de entrega

Incluye esta explicación en el registro de la sesión dentro del repositorio de GitHub, junto al código y las comprobaciones. La entrega es el enlace al repositorio y al commit de la sesión.

Objetivo mínimoError de CORS reproducido en el navegador y comprendido como una restricción de cliente (SOP).
Si lo tienesConfiguración centralizada con WebMvcConfigurer y orígenes externalizados en properties.
RetoAnálisis de la incompatibilidad de seguridad entre comodines y credenciales (allowCredentials) justificado.
Ver respuestas

1 · Porque la Política del Mismo Origen exige coincidencia estricta en los tres componentes del origen: esquema, host y puerto; al diferir el puerto, el navegador aísla los contextos de ejecución.

2 · Utiliza el método OPTIONS, enviando las cabeceras Origin, Access-Control-Request-Method y Access-Control-Request-Headers.

3 · Porque por defecto el navegador solo expone a JavaScript una lista blanca mínima de cabeceras seguras (safelisted headers); para leer cabeceras como Location en una respuesta cross-origin, el servidor debe exponerlas explícitamente.

4 · Previene ataques CSRF y fugas de información, impidiendo que scripts maliciosos de pestañas externas lean datos confidenciales de servicios donde el usuario tiene una sesión activa.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

Debe ser posible distinguir un fallo de red, uno de CORS y una respuesta de error de la API.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.