study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

12 · Integración de extremo a extremo#

Objetivo#

Al terminar esta lección podrás conectar una interfaz web con servicios HTTP de backend construyendo un flujo de datos íntegro y resiliente: desde la interacción del usuario en el navegador hasta la invocación de la API, la validación estricta de la respuesta y la actualización visual de la pantalla. Aprenderás a distinguir rigurosamente los fallos de transporte de los códigos de error HTTP del servidor y de las discrepancias de esquema JSON, a evitar que respuestas lentas y fuera de orden corrompan la vista activa mediante AbortController, y a configurar y diagnosticar las políticas de seguridad de origen cruzado (CORS) en desarrollo y producción.


Antes de empezar#

  1. Completa 05 · Contratos HTTP y ciclo de una solicitud, 10 · TypeScript y el DOM y 11 · Estado y propiedad en React.
  2. Los fragmentos de código de esta lección combinan TypeScript y React (TSX) para modelar la integración en el cliente.
  3. Para reproducir las pruebas de red y de CORS, puedes utilizar un servidor local en Rust (con Actix Web) o simular respuestas HTTP mediante herramientas de desarrollo o pruebas de contrato en tu entorno local.
  4. Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.

Diccionario de conceptos de la lección (Glosario desde cero)#

1. API fetch(), Promesa de red y response.ok#

  • Intuición cotidiana: Enviar a un mensajero a una oficina remota a buscar un paquete con un sobre cerrado. El mensajero regresa y te entrega el sobre cerrado: la misión de transporte fue completada exitosamente. Sin embargo, no sabes si dentro del sobre hay un contrato aprobado o una carta de embargo hasta que lo abres y lees el sello de resolución.
  • Definición técnica precisa:
  • API fetch(): Función estándar de la Web API del navegador que inicia una solicitud HTTP asíncrona a través de la red y devuelve una Promise<Response>.
  • Promesa de red: Objeto asíncrono que representa el ciclo de vida del transporte de red. Solo entra en estado rechazado (rejected) si ocurrió un fallo físico catastrófico que impidió completar la conexión HTTP (como pérdida de señal de red, cable desconectado, DNS inalcanzable, o cancelación por aborto).
  • response.ok: Propiedad booleana de sólo lectura en el objeto Response. Evalúa estrictamente a true si el código de estado HTTP recibido se encuentra dentro del rango de respuestas exitosas:
    response.ok ⟺ 200 ≤ status ≤ 299
  • Ejemplo en código:
TYPESCRIPT
  const response = await fetch("/api/tasks/10");
  if (!response.ok) {
    // Maneja códigos 400, 401, 404, 500, etc.
    throw new Error(`HTTP error: ${response.status}`);
  }
  • Concepción errónea: Pensar que si el servidor responde con 404 Not Found o 500 Internal Server Error, la promesa de fetch() lanzará una excepción y entrará en el bloque catch. Un código 404 o 500 es una respuesta HTTP perfectamente completada; la promesa se resuelve con éxito y debes verificar explícitamente response.ok.

2. Fallo de transporte (Rechazo de promesa) vs Estado de error HTTP (4xx/5xx, Promesa resuelta)#

  • Intuición cotidiana:
  • Fallo de transporte: Intentas llamar por teléfono a un amigo, pero una tormenta derribó la torre celular; la llamada ni siquiera repica ni se conecta ("Fallo de transporte").
  • Estado de error HTTP: La llamada se conecta perfectamente, la otra persona atiende el auricular y te dice con voz clara: "El número que usted marcó no existe en esta oficina" (404) o "Nuestra central de computadoras se incendió" (500). La comunicación telefónica funcionó; lo que falló fue el trámite.
  • Definición técnica precisa:
  • *Fallo de transporte (Transport Failure):* Ruptura en la capa física, de enlace o de transporte (TCP/IP/TLS). Impide que los bytes del mensaje alcancen el servidor o que la respuesta regrese al cliente. En JavaScript produce el rechazo inmediato de la promesa de fetch() con un objeto TypeError ("Failed to fetch").
  • *Estado de error HTTP (HTTP Status Error):* Conexión HTTP completada con éxito donde el servidor web emite una respuesta válida con un código de error de cliente (4xx) o de servidor (5xx). La promesa de fetch() se resuelve normalmente; el cliente tiene la responsabilidad de inspeccionar response.status y response.ok.
  • Ejemplo comparativo:
TYPESCRIPT
  try {
    const res = await fetch("https://api.example.com/data");
    // Si llegamos aquí, NO hubo fallo de transporte
    if (!res.ok) {
      console.error("El servidor respondió con error HTTP:", res.status);
    }
  } catch (err) {
    // Si entramos aquí, falló la conexión física, DNS, TLS o se abortó
    console.error("Fallo catastrófico de transporte de red:", err);
  }
  • Concepción errónea: Capturar cualquier error en un bloque catch genérico y mostrar un mensaje que diga "No tienes conexión a internet". Si el backend respondió un 400 Bad Request por un dato mal escrito, no es un fallo de internet; es un error de validación del cliente.

3. Respuestas sin cuerpo (204 No Content) y error de response.json()#

  • Intuición cotidiana: Pides al mesero que retire un plato sucio de la mesa. El mesero se lleva el plato y te hace un gesto amable con la cabeza en silencio confirmando que está hecho. No esperas que te entregue un documento impreso de 3 páginas confirmando que el plato ya no está ahí. Si intentas forzar una hoja inexistente en una máquina de fax, la máquina se traba.
  • Definición técnica precisa:
  • 204 No Content: Código de estado HTTP exitoso que indica que el servidor ha cumplido con éxito la solicitud (típicamente una eliminación DELETE o una actualización parcial PUT/PATCH), y que el cuerpo del mensaje está estrictamente vacío (cero bytes).
  • Error de response.json(): El método response.json() lee el flujo de bytes del cuerpo de la respuesta y lo pasa al deserializador JSON. Si el cuerpo tiene longitud 0 (como en un 204), el parser de JSON no puede interpretar una cadena vacía como un objeto válido y lanza una excepción fatal: ``text SyntaxError: Unexpected end of JSON input ``
  • Ejemplo en código:
TYPESCRIPT
  async function deleteTask(id: number): Promise<void> {
    const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" });
    if (!res.ok) throw new Error("Delete failed: " + res.status);

    if (res.status === 204) {
      return; // Correcto: no hay cuerpo JSON que parsear
    }
    // await res.json(); // ¡FATAL si el servidor respondió 204!
  }
  • Concepción errónea: Invocar ciegamente await response.json() en todas las respuestas sin comprobar si el código de estado HTTP contempla la presencia de un cuerpo.

4. Condición de carrera en red (Out-of-Order Responses / Race Conditions)#

  • Intuición cotidiana: Dos atletas parten en una carrera hacia la meta. El Atleta 1 corre primero por un camino con barro y pendientes empinadas (una consulta pesada a base de datos de 600 ms). El Atleta 2 parte dos segundos después por una pista recta de asfalto plano (una consulta ligera de 100 ms). El Atleta 2 llega a la meta mucho antes que el Atleta 1. Si el juez premia al último que cruza la línea en lugar de quien cruzó en el turno correcto, premia al equivocado.
  • Definición técnica precisa: Defecto de concurrencia en interfaces de usuario que ocurre cuando múltiples solicitudes asíncronas se disparan sucesivamente sobre la red, pero sus respuestas se completan y resuelven en un orden temporal diferente al orden cronológico en que fueron iniciadas por el usuario. Sin un mecanismo de sincronización, la respuesta lenta y desactualizada de una solicitud anterior puede sobrescribir los datos de una solicitud más reciente.
  • Ejemplo esquemático:
TEXT
  t = 0 ms:   Usuario pulsa Tarea 7  ──> Solicitud A (600 ms) ──────────────> Responde a t = 600 ms
  t = 200 ms: Usuario pulsa Tarea 8  ──> Solicitud B (150 ms) ──> Responde a t = 350 ms
  
  Resultado sin protección:
  A t = 350 ms la UI muestra la Tarea 8.
  A t = 600 ms la Solicitud A sobrescribe la UI, ¡mostrando la Tarea 7 cuando el usuario seleccionó la 8!
  • Concepción errónea: Suponer que la red garantiza la entrega de respuestas en el mismo orden secuencial (FIFO) en que fueron enviadas las peticiones HTTP. Cada solicitud viaja de forma independiente a través de rutas de red y tiempos de base de datos variables.

5. AbortController, AbortSignal y AbortError#

  • Intuición cotidiana: Llamar por teléfono a una pizzería para pedir una pizza familiar. Dos minutos después, tu familia decide salir a cenar fuera. Inmediatamente tomas el teléfono, cancelas la orden antes de que la horneen y cuelgas la llamada ("Abortar la orden").
  • Definición técnica precisa:
  • AbortController: Objeto estándar de la Web API que permite cancelar una o más operaciones asíncronas (como peticiones fetch()) a demanda.
  • AbortSignal: Propiedad del controlador (controller.signal) que se pasa en las opciones de configuración de fetch({ signal }). Permite al cliente de red del navegador monitorear si se ha solicitado una cancelación.
  • AbortError: Cuando se invoca controller.abort(), el navegador interrumpe inmediatamente el flujo de datos del socket de red y rechaza la promesa de fetch() con un objeto DOMException cuyo nombre es "AbortError". El código debe capturar este error específico y descartarlo limpiamente sin mostrarlo al usuario como un fallo del sistema.
  • Ejemplo en código:
TYPESCRIPT
  const controller = new AbortController();
  fetch("/api/tasks", { signal: controller.signal })
    .catch((err) => {
      if (err.name === "AbortError") {
        console.log("Petición cancelada intencionalmente, ignorar");
        return;
      }
      console.error("Error real de red:", err);
    });

  // Cancelar la petición en cualquier momento:
  controller.abort();
  • Concepción errónea: Creer que abortar una solicitud en el cliente (controller.abort()) revierte mágicamente una transacción en la base de datos del backend. Si el backend ya recibió el POST y comenzó a procesar la inserción, abortar en el cliente solo detiene la recepción de la respuesta en el navegador; el servidor ejecutará la acción a menos que implemente cancelación cooperativa a nivel de socket.

6. Origen web (scheme + host + port) y Política del Mismo Origen (Same-Origin Policy - SOP)#

  • Intuición cotidiana: Un edificio residencial con cerraduras independientes en cada apartamento. Los residentes del apartamento 301 no pueden entrar al 302 a abrir los cajones ni leer la correspondencia de sus vecinos, a menos que los vecinos firmen un permiso formal explícito ante la administración del edificio.
  • Definición técnica precisa:
  • *Origen web (Web Origin): La tupla exacta compuesta por tres elementos: esquema (protocolo), nombre de host (dominio/IP) y puerto de red*:
    Origen = langle Esquema, Host, Puerto rangle
  • *Política del Mismo Origen (Same-Origin Policy o SOP):* Mecanismo de seguridad crítico implementado en el núcleo de todos los navegadores web modernos. Prohíbe que un script JavaScript ejecutado en un origen (ej. http://localhost:3000) pueda leer el contenido de las respuestas obtenidas desde un origen diferente (ej. http://localhost:8080), a menos que el segundo origen declare formalmente su autorización.
  • Comparación de orígenes contra http://example.com:80/app:
  • http://example.com:80/other: Mismo origen (mismo esquema, host y puerto).
  • https://example.com:80/app: Distinto origen (esquema https vs http).
  • http://api.example.com:80/app: Distinto origen (subdominio api.example.com vs example.com).
  • http://example.com:8080/app: Distinto origen (puerto 8080 vs 80).
  • Concepción errónea: Pensar que dos servicios ejecutándose en la misma máquina física (localhost:3000 y localhost:8080) pertenecen al mismo origen. Al tener puertos distintos, el navegador los trata como entidades completamente aisladas e independientes.

7. CORS (Cross-Origin Resource Sharing) y cabeceras Access-Control-Allow-*#

  • Intuición cotidiana: Una visa de entrada sellada en tu pasaporte: cuando llegas a la aduana de otro país, el oficial de migración inspecciona el sello oficial de su propio gobierno autorizando tu entrada. Si el sello no está, la policía del aeropuerto te impide salir a la ciudad.
  • Definición técnica precisa: Especificación del W3C que permite relajar de forma segura las restricciones de la Política del Mismo Origen (SOP). Funciona mediante el intercambio de cabeceras HTTP especiales devueltas por el servidor de destino para indicarle al motor del navegador si el código JavaScript del origen emisor tiene permiso para inspeccionar la respuesta:
  • Access-Control-Allow-Origin: Especifica qué orígenes tienen autorización para leer la respuesta (ej. http://localhost:3000 o https://app.example.com).
  • Access-Control-Allow-Methods: Métodos HTTP admitidos (ej. GET, POST, PUT, DELETE, OPTIONS).
  • Access-Control-Allow-Headers: Cabeceras de solicitud autorizadas (ej. Content-Type, Authorization).
  • Ejemplo de cabecera emitida por el backend:
HTTP
  Access-Control-Allow-Origin: http://localhost:3000
  Access-Control-Allow-Methods: GET, POST, OPTIONS
  Access-Control-Allow-Headers: Content-Type
  • Concepción errónea: Creer que CORS es un mecanismo de autenticación o seguridad para proteger a tu API de atacantes. CORS protege al usuario del navegador contra sitios maliciosos, no al servidor. Una herramienta de línea de comandos como curl, Postman o un script en Python ignora por completo las reglas de CORS y puede atacar el servidor libremente. El servidor debe protegerse con autenticación y autorización real.

8. Solicitud Preflight (OPTIONS)#

  • Intuición cotidiana: Enviar un emisario con bandera blanca a las puertas de un castillo para preguntar: "¿Nos permitirán entrar con una carga de mercancías empaquetadas en cajas especiales?". Si los guardias responden afirmativamente, la caravana principal avanza; si responden que no, la caravana ni siquiera se acerca a la muralla.
  • Definición técnica precisa: Solicitud preliminar generada automáticamente por el navegador utilizando el método HTTP OPTIONS antes de ejecutar la petición real del script. El navegador la dispara obligatoriamente siempre que la solicitud de origen cruzado no se clasifique como "solicitud simple" (Simple Request). Se dispara cuando:
  1. Se utiliza un método distinto de GET, HEAD o POST (por ejemplo: PUT, DELETE, PATCH).
  2. Se envía una cabecera personalizada (por ejemplo: Authorization, Idempotency-Key).
  3. La cabecera Content-Type es diferente de los tres tipos MIME simples (por ejemplo: al usar application/json).
  • Ejemplo en el cable:
HTTP
  OPTIONS /api/tasks HTTP/1.1
  Host: localhost:8080
  Origin: http://localhost:3000
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: Content-Type
  • Concepción errónea: Pensar que la solicitud preflight es un bug o un error en tu código de frontend. Es un mecanismo de seguridad impuesto por la especificación del navegador para asegurarse de que el servidor entiende y acepta peticiones complejas antes de ejecutarlas.

9. Modo no-cors y respuesta opaca (Opaque Response)#

  • Intuición cotidiana: Pedir un paquete por correo confidencial donde la caja está recubierta de plomo macizo sin ventanas. Puedes confirmar que la caja fue entregada al buzón, pero está terminantemente prohibido abrirla, pesarla o mirar qué hay adentro. Para tu código JavaScript, el paquete es un bloque ciego e inútil.
  • Definición técnica precisa: Opción de configuración en fetch(url, { mode: "no-cors" }) diseñada exclusivamente para casos de uso limitados donde solo se desea disparar una acción ciega o almacenar en caché de service worker un recurso externo (como un script o un archivo CSS estático). Bajo este modo, el navegador emite una *respuesta opaca (Opaque Response)*:
  • response.status devuelve 0.
  • response.ok evalúa a false.
  • El cuerpo de la respuesta está completamente bloqueado para JavaScript: invocar response.json() o response.text() falla de inmediato.
  • Concepción errónea: Pensar que añadir mode: "no-cors" "arregla el error de CORS con tu API". Añadir mode: "no-cors" hace que sea absolutamente imposible para tu aplicación leer los datos JSON devueltos por el servidor.

10. Proxy de desarrollo vs CORS en producción#

  • Intuición cotidiana:
  • Proxy de desarrollo: Un túnel secreto dentro de tu propio edificio que conecta directamente la cocina con el almacén sin tener que salir a la calle pública. Como no sales a la calle, los guardias fronterizos de la calle ni se enteran.
  • CORS en producción: El almacén está formalmente en otra ciudad y tiene una aduana oficial en la puerta que revisa tu pasaporte antes de darte la mercancía.
  • Definición técnica precisa:
  • Proxy de desarrollo: Configuración en herramientas locales (como el servidor de desarrollo de Vite o Webpack) donde el navegador envía todas las peticiones a su propio origen local (http://localhost:3000/api/...). El servidor de desarrollo de Node.js intercepta la petición y la reenvía por detrás a nivel de socket TCP al backend (http://localhost:8080/api/...). Para el navegador, la petición es *mismo origen (same-origin)*, eliminando por completo los problemas de CORS durante el desarrollo.
  • CORS en producción: Cuando en producción el frontend se aloja en un dominio o CDN (ej. https://app.example.com) y la API en otro (ej. https://api.example.com), el backend debe configurar cabeceras Access-Control-Allow-Origin explícitas autorizando dicho dominio. Alternativamente, un proxy inverso de producción (Nginx, Envoy, Cloudflare) puede unificar ambos servicios bajo un mismo dominio (ej. example.com/api).
  • Concepción errónea: Configurar Access-Control-Allow-Origin: * (comodín universal) en producción como solución perezosa. El comodín impide el uso de credenciales y cookies seguras, y expone tu API a accesos de origen cruzado desde cualquier sitio web no autorizado.

Modelo mental y desempacado técnico paso a paso#

1. Cronología milisegundo a milisegundo de la condición de carrera (Task 7 vs Task 8)#

Examinemos con precisión de reloj digital qué ocurre en la memoria y en la pantalla del usuario cuando se producen peticiones de red asíncronas no sincronizadas:

DIAGRAMA MERMAID
sequenceDiagram
    autonumber
    actor U as Usuario
    participant React as Componente UI
    participant Net as Capa de Red (fetch)
    participant Srv as Servidor / Base de Datos

    U->>React: Hace clic en "Ver Tarea 7"
    React->>Net: fetchTask(7) [Solicitud Lenta: 600 ms]
    Note over React: UI en estado: Loading Tarea 7...

    U->>React: Cambia de idea a los 200 ms: Clic en "Ver Tarea 8"
    React->>Net: fetchTask(8) [Solicitud Rápida: 150 ms]
    Note over React: UI en estado: Loading Tarea 8...

    Net->>Srv: Procesa Tarea 8 (Caché caliente: 150 ms)
    Srv-->>Net: Retorna HTTP 200 { id: 8, title: "Tarea 8" } a los 350 ms
    Net-->>React: Promesa de Tarea 8 se resuelve
    React->>React: setState({ kind: "ready", task: Task 8 })
    Note over React: Pantalla muestra correctamente: "Tarea 8"

    Net->>Srv: Procesa Tarea 7 (Consulta pesada en DB: 600 ms)
    Srv-->>Net: Retorna HTTP 200 { id: 7, title: "Tarea 7" } a los 600 ms
    Net-->>React: Promesa de Tarea 7 se resuelve (¡Tardía!)
    Note over React,Net: ¡FALLO CATASTRÓFICO SIN PROTECCIÓN!
    React->>React: setState({ kind: "ready", task: Task 7 })
    Note over React: La pantalla se corrompe: muestra "Tarea 7" aunque la barra de selección dice Tarea 8

La defensa en profundidad: AbortController + Bandera de actualidad (active)#

Para neutralizar completamente esta anomalía de carrera, la arquitectura combina dos mecanismos complementarios dentro del ciclo de vida de useEffect:

TYPESCRIPT
useEffect(() => {
  const controller = new AbortController();
  let active = true; // Bandera de actualidad del closure

  setState({ kind: "loading" });

  fetchTask(taskId, controller.signal)
    .then((task) => {
      // Si el componente ya cambió a otro ID o se desmontó,
      // active será false; descartamos el resultado silenciosamente.
      if (active) {
        setState(task ? { kind: "ready", task } : { kind: "missing" });
      }
    })
    .catch((err: unknown) => {
      // Ignoramos el error de cancelación intencional
      if (active && !controller.signal.aborted) {
        setState({ kind: "error", message: "Failed to load task" });
      }
    });

  // Función de limpieza simétrica:
  return () => {
    active = false;      // 1. Invalida de inmediato cualquier callback pendiente
    controller.abort();  // 2. Cancela el socket de red en el navegador
  };
}, [taskId]);
  1. Cuando el usuario hace clic en la Tarea 8 (t = 200 ms): React ejecuta la función de limpieza del efecto anterior correspondiente a la Tarea 7: - Marca active = false. - Llama a controller.abort(), cerrando el socket de la solicitud de la Tarea 7 en el navegador para ahorrar ancho de banda.
  2. Si la respuesta de la Tarea 7 ya había llegado al buffer del navegador antes del aborto: Cuando la promesa de la Tarea 7 intenta ejecutarse, evalúa if (active). Como active es false, el bloque se ignora por completo. La pantalla continúa mostrando de forma inmutable la Tarea 8 seleccionada por el usuario.

2. Las tres fronteras de fallo de una petición HTTP#

Una operación de integración con backend atraviesa tres fronteras de fallo completamente independientes, cada una de las cuales requiere un tratamiento técnico diferente:

DIAGRAMA MERMAID
flowchart TD
    Start["fetch(url)"] --> F1{"Frontera 1: Red y Transporte"}
    F1 -->|DNS caído / Sin WiFi / Abort| E1["Promesa rechazada (TypeError / AbortError)"]
    F1 -->|Socket TCP conectado con éxito| F2{"Frontera 2: Protocolo HTTP"}

    F2 -->|response.status === 404| R_Missing["Estado de Negocio: Missing / Not Found"]
    F2 -->|response.ok === false (4xx/5xx)| E2["Error de Protocolo (HTTP Error Status)"]
    F2 -->|response.status === 204| R_Empty["Éxito sin cuerpo (No intentar parsear JSON)"]
    F2 -->|response.ok === true (200 OK)| F3{"Frontera 3: Esquema de Datos"}

    F3 -->|JSON malformado / SyntaxError| E3["Error de Parseo Sintáctico"]
    F3 -->|isTask(payload) === false| E4["Error de Integridad de Esquema"]
    F3 -->|isTask(payload) === true| R_Success["Estado Visual: Ready con datos validados"]
  1. Frontera 1 (Transporte de red): Se manifiesta como una promesa rechazada en el bloque catch. Ocurre antes de que el servidor web pueda siquiera emitir una cabecera HTTP.
  2. Frontera 2 (Protocolo HTTP y Semántica): La promesa se resuelve. Se inspeccionan response.status y response.ok. Un 404 no es necesariamente una excepción del sistema; puede ser el resultado legítimo de negocio: "La tarea solicitada no existe". Un 204 indica éxito sin datos. Un 500 requiere reportar un error no recuperable y ofrecer un reintento.
  3. Frontera 3 (Contrato de serialización e integridad): El servidor respondió 200 OK, pero el cuerpo devuelto debe parsearse con seguridad e inspeccionarse con un predicado de tipo (isTask). Si el backend sufrió una regresión y devolvió { id: null }, el cliente debe clasificarlo como un fallo de esquema antes de renderizar.

3. Por qué CORS lo impone el navegador y qué hace el Preflight OPTIONS#

Para desmitificar los errores de CORS, comprendamos el ciclo de vida de una solicitud compleja:

DIAGRAMA MERMAID
sequenceDiagram
    autonumber
    participant JS as Script de Frontend (http://localhost:3000)
    participant Engine as Motor del Navegador (Chrome/Firefox)
    participant API as Servidor Backend (http://localhost:8080)

    JS->>Engine: fetch("http://localhost:8080/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" } })
    Note over Engine: Motor detecta: Cross-Origin Y cabecera personalizada application/json
    
    Engine->>API: 1. Preflight OPTIONS /api/tasks (Origin: http://localhost:3000)
    
    alt Backend configurado con CORS
        API-->>Engine: HTTP 204 (Access-Control-Allow-Origin: http://localhost:3000, Allow-Methods: POST, Allow-Headers: Content-Type)
        Note over Engine: Permiso concedido por el servidor
        Engine->>API: 2. Petición Real: POST /api/tasks con cuerpo JSON
        API-->>Engine: HTTP 201 Created { id: 101, title: "New Task" }
        Engine-->>JS: Promesa resuelta: entrega respuesta al código JavaScript
    else Backend sin soporte CORS
        API-->>Engine: HTTP 404 / 405 / 200 sin cabecera Access-Control-Allow-Origin
        Note over Engine: ¡BLOQUEO DE SEGURIDAD! El motor del navegador destruye la respuesta
        Engine-->>JS: Promesa rechazada: TypeError: Failed to fetch (CORS header missing)
    end

Revelaciones esenciales sobre CORS:#

  1. La petición real puede ejecutarse en el backend: Si la petición era una solicitud simple (como un GET o un POST con application/x-www-form-urlencoded), el navegador envía la petición al servidor y el servidor la procesa por completo. Sin embargo, cuando la respuesta regresa al navegador, el motor inspecciona si viene la cabecera Access-Control-Allow-Origin. Si no viene, el navegador destruye la respuesta y emite un error en la consola. Por eso, CORS no impide que una llamada llegue al servidor; impide que el script de JavaScript del navegador lea el resultado.
  2. CORS no es un firewall ni un sistema de login: Un atacante no necesita un navegador para vulnerar tu API; puede abrir una terminal y ejecutar: ``bash curl -X POST https://api.example.com/tasks -d '{"title":"Attack"}' ` curl` no es un navegador web; no implementa la Política del Mismo Origen ni consulta cabeceras CORS. Tu backend debe protegerse siempre mediante autenticación y autorización criptográfica (tokens, sesiones, verificación de roles).

Diagrama#

DIAGRAMA MERMAID
sequenceDiagram
    participant U as Usuario
    participant UI as Interfaz
    participant API as API HTTP
    participant DB as Base de datos
    U->>UI: Enviar formulario
    UI->>API: POST JSON
    API->>DB: Validar y persistir
    DB-->>API: Resultado
    API-->>UI: Estado HTTP y cuerpo
    UI->>UI: Validar JSON y elegir estado visual
    UI-->>U: Éxito, ausencia o error

Cada flecha es una frontera con formato, error y tiempo propios.

Diagrama estático de integración en el navegador
Diagrama estático de integración en el navegador

Fuente editable: Mermaid.


Ejemplo razonado#

Este módulo TypeScript/TSX implementa el ciclo completo de consumo de un endpoint de detalle de tareas, manejando unión discriminada de estados, cancelación con AbortController, verificación de contratos HTTP y comprobación de esquema runtime:

TSX
import { useEffect, useState } from "react";

// 1. Data Contract and Runtime Type Guard
export type Task = {
  id: number;
  title: string;
  status: "open" | "done";
};

function isTask(value: unknown): value is Task {
  if (typeof value !== "object" || value === null) return false;
  const candidate = value as Record<string, unknown>;
  return (
    typeof candidate.id === "number" &&
    typeof candidate.title === "string" &&
    (candidate.status === "open" || candidate.status === "done")
  );
}

// 2. Discriminated Union for Comprehensive Network State
export type TaskState =
  | { kind: "idle" }
  | { kind: "loading" }
  | { kind: "ready"; task: Task }
  | { kind: "missing" }
  | { kind: "error"; message: string };

// 3. Resilient Network Fetcher across the Three Failure Boundaries
export async function fetchTask(taskId: number, signal: AbortSignal): Promise<Task | null> {
  const response = await fetch(`/api/tasks/${taskId}`, { signal });

  // Boundary 2A: Semantic Absence (Contractually defined 404)
  if (response.status === 404) {
    return null;
  }

  // Boundary 2B: HTTP Error Statuses (4xx, 5xx)
  if (!response.ok) {
    throw new Error(`Server returned error status: ${response.status}`);
  }

  // Boundary 2C: Empty Body Contracts
  if (response.status === 204) {
    throw new Error("Unexpected empty body: 204 received for entity details endpoint");
  }

  // Boundary 3: Deserialization and Runtime Schema Validation
  let payload: unknown;
  try {
    payload = await response.json();
  } catch (err: unknown) {
    throw new Error("Corrupted payload: Response body is not valid JSON");
  }

  if (!isTask(payload)) {
    throw new Error("Schema integrity violation: Payload does not conform to Task contract");
  }

  return payload;
}

// 4. React Component with Race Condition Prevention and Accessible Feedback
export function TaskDetailsViewer({ taskId }: { taskId: number }) {
  const [state, setState] = useState<TaskState>({ kind: "loading" });

  useEffect(() => {
    const controller = new AbortController();
    let active = true;

    setState({ kind: "loading" });

    fetchTask(taskId, controller.signal)
      .then((task) => {
        if (active) {
          setState(task ? { kind: "ready", task } : { kind: "missing" });
        }
      })
      .catch((error: unknown) => {
        // Discard intentional cancellations silently
        if (active && !controller.signal.aborted) {
          const detail = error instanceof Error ? error.message : "An unexpected network error occurred";
          console.error("Task loading failure:", error);
          setState({ kind: "error", message: detail });
        }
      });

    // Cleanup: cancel pending network socket and mark closure as inactive
    return () => {
      active = false;
      controller.abort();
    };
  }, [taskId]);

  // Exhaustive UI Rendering based on Discriminated Union
  if (state.kind === "loading") {
    return <p aria-busy="true">Loading task details...</p>;
  }

  if (state.kind === "missing") {
    return <p role="status">The requested task could not be found.</p>;
  }

  if (state.kind === "error") {
    return (
      <div role="alert" style={{ color: "#c92a2a", border: "1px solid #c92a2a", padding: "0.75rem" }}>
        <strong>Error:</strong> {state.message}
      </div>
    );
  }

  if (state.kind === "ready") {
    return (
      <article>
        <h3>{state.task.title}</h3>
        <p>Status: <strong>{state.task.status}</strong></p>
      </article>
    );
  }

  return null;
}

Por qué esta integración es resiliente:#

  • Distingue rigurosamente la ausencia semántica (404 arrow kind: "missing") del error de protocolo (500 arrow kind: "error").
  • Protege la deserialización con try/catch para evitar excepciones no controladas si el backend devuelve HTML en una página de error 502.
  • Emplea AbortController junto a active = false en la función de limpieza, impidiendo que solicitudes rezagadas sobreescriban la tarea seleccionada.
  • Utiliza roles de accesibilidad semánticos (role="alert" para errores, aria-busy="true" durante la carga) para mantener informadas a las tecnologías de asistencia.

Práctica independiente: Ciclo RED arrow GREEN arrow REFACTOR#

Diseña una suite de integración para el endpoint GET /api/tasks/:id simulando clientes de red y validando los siguientes escenarios.

Fase 1: RED (Matriz de predicciones obligatoria)#

Completa en tu cuaderno técnico la matriz de resultados antes de programar la integración:

#Escenario simuladoRespuesta HTTP devueltaCuerpo devueltoEstado visual esperado (state.kind)Mensaje presentado al usuario
1Registro existente200 OK{"id": 7, "title": "Buy bread", "status": "open"}readyTítulo y estado renderizados normalmente.
2Registro inexistente404 Not Found{"detail": "Task 99 not found"}missing"The requested task could not be found."
3Caída de base de datos503 Service Unavailable{"error": "Database down"}errorAlerta legible con opción de reintento.
4Servidor devuelve HTML corrupto200 OK<html><body>Bad Gateway</body></html>error"Corrupted payload: Response body is not valid JSON".
5Carrera de peticiones concurrentesTarea 7 (tarda 600 ms) seguida de Tarea 8 (tarda 150 ms)Respuestas válidas para ambos IDsready con ID 8La pantalla jamás debe mostrar la Tarea 7 tras mostrar la 8.

Fase 2: GREEN (Implementación de fetcher y guarda de actualidad)#

  • Implementa fetchTask con comprobación de response.ok y validación con isTask.
  • Implementa TaskDetailsViewer con el useEffect protegido por AbortController y la bandera active.
  • Comprueba que todos los escenarios de la matriz produzcan el estado correspondiente.

Fase 3: REFACTOR (Taller de configuración de CORS)#

  • Configura tu backend en el puerto 8080 y el cliente en el 3000.
  • Observa en la pestaña Network de DevTools la aparición del método preliminar OPTIONS (Preflight).
  • Agrega las cabeceras requeridas en el backend (Access-Control-Allow-Origin, Access-Control-Allow-Headers) y verifica la desaparición del error en la consola.
  • Compara esta solución contra un proxy local configurado en Vite (server.proxy: { '/api': 'http://localhost:8080' }).

Registra tus notas antes de abrir la guía de soluciones de frontend.


Preguntas de comprensión profunda y diagnóstico socrático#

Reflexiona sobre estas cinco preguntas de razonamiento técnico antes de avanzar:

Pregunta 1: La trampa de try/catch con la API fetch()#

Un desarrollador escribe este servicio de datos:

TYPESCRIPT
async function loadUserData() {
  try {
    const response = await fetch("/api/user");
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error: "Network error occurred" };
  }
}
  • Si el servidor responde con 500 Internal Server Error devolviendo un JSON con { "message": "Crash" }, ¿entrará la ejecución en el bloque catch?
  • ¿Qué valor devolverá la función loadUserData al llamador?
  • ¿Por qué este patrón oculta errores críticos del servidor haciéndolos pasar por llamadas exitosas?

Pregunta 2: El desastre de la respuesta fuera de orden (Race Condition)#

  • Describe la secuencia exacta de eventos que provoca que una respuesta HTTP vieja sobrescriba a una nueva cuando un usuario hace clic rápidamente en los elementos de una lista.
  • ¿Por qué un simple AbortController puede no ser suficiente si el navegador cancela la conexión pero el evento de resolución ya estaba en la cola de microtareas de JavaScript (Microtask Queue)?
  • ¿Qué función cumple la variable local booleana active = false en la función de limpieza de useEffect para garantizar inmunidad total contra este problema?

Pregunta 3: El crash silencioso de response.json() en respuestas 204 No Content#

Un endpoint para eliminar tareas DELETE /api/tasks/42 responde exitosamente con 204 No Content. El cliente ejecuta:

TYPESCRIPT
const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" });
const json = await res.json();
  • ¿Qué excepción lanza el motor de JavaScript en la segunda línea y por qué?
  • ¿Por qué el código de estado 204 exige que el desarrollador no intente leer el cuerpo de la respuesta?

Pregunta 4: La falacia de CORS como mecanismo de seguridad del servidor#

Un equipo de seguridad audita una API bancaria y descubre que el endpoint /transfer no requiere token de sesión si la petición no incluye cabeceras CORS. El programador se defiende diciendo: "Nuestro backend tiene CORS habilitado solo para nuestro dominio banco.com; nadie desde otro sitio web puede hacer transferencias".

  • Desmonta esta afirmación demostrando cómo un atacante puede ejecutar una transferencia utilizando curl o un script en Python desde una terminal.
  • ¿Quién es el agente responsable de verificar y aplicar las restricciones de CORS: el servidor o el navegador del cliente?
  • ¿A quién protege realmente la Política del Mismo Origen (SOP) y qué ataque clásico web (Cross-Site Request Forgery / CSRF) busca mitigar?

Pregunta 5: El peligro del comodín Access-Control-Allow-Origin: * con credenciales#

  • Si una API devuelve la cabecera Access-Control-Allow-Origin: *, ¿qué ocurre si el frontend intenta enviar credenciales o cookies seguras mediante fetch(url, { credentials: "include" })?
  • ¿Por qué la especificación oficial de CORS del W3C prohíbe terminantemente la combinación de Access-Control-Allow-Origin: * con Access-Control-Allow-Credentials: true?

Code smell#

Síntoma: Colapso de errores de red en listas vacías (The Silent Empty List Antipattern)#

Un componente captura cualquier fallo en su función de carga y devuelve una lista vacía:

TYPESCRIPT
fetch("/api/tasks")
  .then(res => res.json())
  .then(data => setTasks(data))
  .catch(() => setTasks([])); // "Para no asustar al usuario con un error"
  • Evidencia observable: Cuando el servidor de base de datos colapsa o el usuario pierde la conexión WiFi, la interfaz muestra alegremente: "¡No tienes ninguna tarea pendiente! Disfruta tu día libre".
  • Hipótesis del problema: Los desarrolladores eliminaron la distinción semántica entre un resultado legítimo vacío (cero tareas en la base de datos) y un fallo catastrófico del sistema, ocultando la avería detrás de un estado de éxito fingido.
  • Consecuencia concreta: Pérdida masiva de confianza del usuario: el cliente asume que sus datos se borraron permanentemente o ignora que sus tareas urgentes están pendientes de ejecución.
  • Tratamiento mínimo justificado: Representar explícitamente el estado de error mediante una unión discriminada ({ kind: "error", message: "..." }), mostrar un mensaje transparente de fallo en la interfaz con un rol accesible (role="alert"), y proporcionar un botón visible de reintento manual.

Criterio de salida#

Puedes considerar superada esta lección cuando:

  • Tu arquitectura de red distingue con precisión los tres tipos de fallos: caída de transporte, códigos de error HTTP (4xx/5xx) y fallos de validación de esquema JSON.
  • Representas el ciclo de red en React mediante una unión discriminada (loading, ready, missing, error), evitando combinaciones de booleanos imposibles.
  • Utilizas AbortController junto a una bandera de limpieza para evitar condiciones de carrera cuando las peticiones regresan fuera de orden.
  • No invocas response.json() en respuestas con código 204 No Content.
  • Puedes explicar con precisión cómo opera el preflight OPTIONS en CORS y cómo configurar un proxy de desarrollo para trabajar con el mismo origen local.

Lecturas oficiales#


Siguiente paso#

Continúa con 13 · Code smells y refactorización: aprenderás a diagnosticar malos olores de diseño y acoplamiento entre capas arquitectónicas basándote en evidencia observable y pruebas reproducibles.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)