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:
- 1. fetch(url) → Espera cabeceras de red
- 2. response.ok / response.status
- 3. response.json() → Espera descarga y parseo de bytes
- 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:
- Esquema / Protocolo (http://)
- Host / Dominio (localhost)
- 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 |
SÍ | 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:
- Cuando el frontend envía una petición compleja (por ejemplo, un
POSTconContent-Type: application/jsono unPUT/DELETE), el navegador no lanza elPOSTdirectamente. - Primero envía automáticamente una petición de sondeo o verificación previa (Preflight Request) con el método HTTP
OPTIONS. - El navegador le pregunta al servidor: «Oye, backend, tengo un script en
http://localhost:5500que quiere enviarte un POST con JSON. ¿Me autorizas?». - El servidor responde con las cabeceras de autorización:
Access-Control-Allow-Origin: http://localhost:5500Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, Authorization
- 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
- 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.
- 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. - 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.html→ Open with Live Server enhttp://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 olvidafetch()solo rechaza la promesa ante un fallo de red. Un404o un500son respuestas perfectamente válidas parafetch, 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.awaitdos veces- El primer
awaitespera a que lleguen las cabeceras y el estado. El segundo, el deres.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:
-
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».
-
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).
- Status: Debe marcar
-
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.
-
Analiza la pestaña Preview / Response:
- Comprueba que visualizas el árbol de objetos JSON exactamente como lo programaste en Spring Boot.
-
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.
- Modifica la generación de cada elemento de la lista para que incluya un botón «Ver tareas».
- Al pulsarlo, lanza una segunda llamada a
/api/v1/proyectos/{id}/tareasy renderiza las tareas en una sublista bajo el proyecto. - 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ó.
- 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.
- Pide un proyecto que no exista (
/api/v1/proyectos/9999/tareas) y comprueba que tu página muestra el404en lugar de quedarse pensando. Ese404es la regla que implementaste en la sesión 29: ahora la estás viendo desde el otro lado. - 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:
- Crea un endpoint en Spring Boot.
- Abre Postman o Bruno, envía la petición y recibe un maravilloso
200 OK. - 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:5500yhttp://127.0.0.1:5500son orígenes distintos aunque apunten a la misma máquina, yhttp://localhost:5500yhttps://localhost:5500tambié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
Locationdel201 Createdllega 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
OPTIONSextra 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
- Reinicia tu aplicación Spring Boot.
- Vuelve a tu navegador en
http://localhost:5500y pulsa Actualizar lista. - Comprueba que el mensaje rojo de CORS ha desaparecido por completo y los proyectos se renderizan.
- 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
- En Response Headers verás la cabecera emitida por Spring Boot:
- 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.
-
En DevTools → Network, marca la casilla Preserve log y filtra por
Fetch/XHR. -
Desde tu página, lanza un
POSTconContent-Type: application/json(el botón de crear proyecto). -
Ahora sí aparecen dos líneas para una sola operación:
# Método Ruta Estado Qué es 1 OPTIONS/api/v1/proyectos200El navegador preguntando «¿me dejas?» 2 POST/api/v1/proyectos201La petición de verdad, ya autorizada -
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. -
Lo importante: ese
OPTIONSlo 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 delPOSThasta 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
- Añade
http://localhost:3000a la lista de orígenes enapplication.properties. - Reinicia y comprueba que los tres orígenes (
5500,127.0.0.1:5500y3000) son aceptados. - Sirve temporalmente la página desde
http://localhost:9999(por ejemplo,python -m http.server 9999desdecliente), 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. - 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 temporalmenteexposedHeaders("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. - 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 policypero la pestaña Network muestra que el servidor respondió200. - El servidor responde
404y la página se queda en blanco.
- 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+POSTen DevTools con tus propios ojos; sabes provocar y reconocer un bloqueo de CORS; tu página lee la cabeceraLocation; 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
- Repite la misma consulta desde el cliente HTTP y desde el navegador, y distingue respuesta del servidor de bloqueo de lectura por CORS.
- 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:
- En la pestaña Network de DevTools, localiza el selector de Throttling (por defecto en No throttling).
- Cámbialo a Slow 3G (3G lenta) y pulsa Actualizar lista.
- Observa en la columna Waterfall (cascada) cómo el tiempo de espera (TTFB - Time to First Byte) se dispara a varios segundos.
- ¿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?
index.html consumiendo GET /api/v1/proyectos con fetch nativo servida en servidor local.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ónAuthorization), el navegador rechaza terminantemente el uso del comodínallowedOrigins("*").
Investiga y responde con criterio técnico:
- ¿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)? - ¿Por qué Spring Boot lanza una excepción al arrancar si detectas que has configurado simultáneamente
allowedOrigins("*")yallowCredentials(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.
WebMvcConfigurer y orígenes externalizados en properties.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.