05 · Contratos HTTP y ciclo de una solicitud#
Objetivo#
Al terminar esta lección podrás seguir con precisión microscópica el viaje de una solicitud HTTP desde que los bytes viajan por el socket TCP del sistema operativo hasta que se transforman en una respuesta estructurada. Aprenderás a definir qué significa cada método y código de estado HTTP bajo el estándar RFC 9110, a distinguir rigurosamente la validación sintáctica de la semántica de dominio, y a diseñar una arquitectura de idempotencia a nivel de base de datos para operaciones críticas de escritura.
Un contrato HTTP profesional incluye la anatomía del éxito, la tipificación formal del error y la representación explícita de la ausencia; no es un simple intercambio informal de objetos JSON.
Antes de empezar#
- Completa las lecciones del Módulo 01 sobre tipos,
Result,Option, structs y pruebas unitarias. - Esta lección aborda el protocolo y el diseño formal del contrato sin requerir un framework web en ejecución; la integración con Actix Web se desarrolla en la Lección 06.
- Los mensajes HTTP en crudo y las respuestas mostradas en esta lección son fragmentos ilustrativos del protocolo en el cable, diseñados para entender la serialización binaria y textual de la red.
- Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.
Diccionario de conceptos de la lección (Glosario desde cero)#
1. Protocolo HTTP y mensaje en el cable (HTTP Wire Protocol)#
- Intuición cotidiana: Es el formato estandarizado de una carta postal certificada: un encabezado superior visible con destinatario y remitente, un espacio en blanco obligatorio de separación, y el pliego interior con el contenido real.
- Definición técnica precisa: Protocolo de la capa de aplicación (modelo OSI/TCP) basado en texto o tramas binarias que opera mediante un patrón solicitud-respuesta sobre una conexión de transporte confiable (típicamente TCP en HTTP/1.1 y HTTP/2, o QUIC/UDP en HTTP/3). En HTTP/1.1, un mensaje en el cable se divide en cuatro partes secuenciales:
- *Línea inicial (Start-line): Para solicitudes es la línea de petición (Request-line:
MÉTODO /ruta HTTP/1.1\r\n); para respuestas es la línea de estado (Status-line*:HTTP/1.1 CÓDIGO MOTIVO\r\n). - *Cabeceras (Headers):* Pares clave-valor (
Nombre: Valor\r\n) que transmiten metadatos de transporte, codificación y seguridad. - Separador de fin de cabeceras: La secuencia estricta de retorno de carro y salto de línea doble:
\r\n\r\n(bytes0x0D 0x0A 0x0D 0x0A). - *Cuerpo (Message Body): Secuencia de bytes opcional que contiene la carga útil (payload*), cuya longitud viene dada por la cabecera
Content-Lengtho por fragmentos conTransfer-Encoding: chunked.
- Ejemplo en el cable (bytes textuales):
POST /tasks HTTP/1.1\r\n
Host: api.example.com\r\n
Content-Type: application/json\r\n
Content-Length: 26\r\n
\r\n
{"title":"Deploy service"}
- Concepción errónea: Pensar que HTTP es JSON. HTTP es el protocolo de transporte de mensajes; JSON es simplemente un formato de texto estructurado que puede o no viajar dentro del cuerpo del mensaje HTTP.
2. Método / Verbo HTTP (GET, POST, PUT, PATCH, DELETE)#
- Intuición cotidiana: El verbo de acción en una orden formal: "consultar expediente" (
GET), "entregar solicitud de registro" (POST), "reemplazar expediente completo" (PUT), "actualizar un campo del expediente" (PATCH), o "destruir expediente" (DELETE). - Definición técnica precisa: Token en la línea inicial que indica la semántica primaria que el cliente espera que el servidor aplique sobre el recurso identificado por el URI (RFC 9110, sección 9). Cada método define expectativas formales sobre seguridad de lectura e idempotencia en la manipulación del recurso.
- Ejemplo en código:
GET /tasks/42 HTTP/1.1
DELETE /tasks/42 HTTP/1.1
- Concepción errónea: Creer que el servidor ejecuta automáticamente la acción por el solo hecho de usar el verbo. El método es un contrato declarativo; el software del servidor es responsable de validar permisos, verificar el estado y ejecutar la operación correspondiente.
3. Método seguro (Safe) vs Idempotente (f(f(x)) = f(x))#
- Intuición cotidiana:
- Seguro: Mirar el saldo en el cajero automático; no altera el dinero de la cuenta sin importar cuántas veces mires.
- Idempotente: Presionar el interruptor de apagado de la luz; si la luz ya está apagada, presionar "apagar" diez veces más deja la habitación exactamente en el mismo estado apagado.
- Definición técnica precisa:
- *Método seguro (Safe Method):* Método cuya semántica está definida como de solo lectura y no causa ninguna mutación de estado observable en el recurso de origen (RFC 9110, §9.2.1). Ejemplos:
GET,HEAD,OPTIONS. (Los efectos secundarios de infraestructura como métricas, logs o caché no se consideran cambios de estado del recurso). - *Método idempotente (Idempotent Method): Método donde el efecto previsto sobre el servidor de N > 0 solicitudes idénticas es exactamente el mismo que el de una sola solicitud: f(f(x)) = f(x) (RFC 9110, §9.2.2). Ejemplos:
PUT,DELETE,GET.POSTno* es idempotente por estándar, pues múltiples llamadas típicamente acumulan múltiples creaciones o cobros. - Ejemplo formal:
DELETE /tasks/42: La primera llamada borra la fila (devuelve204 No Contento200 OK). Las siguientes llamadas encuentran que ya no existe (pueden devolver404 Not Found), pero el estado final del servidor sigue siendo el mismo: la fila 42 no existe. Es idempotente.POST /transfers: Si se envían 3 solicitudes idénticas sin mecanismos de protección, se debitará el dinero 3 veces. No es idempotente.- Concepción errónea: Creer que idempotencia exige que el código de estado y el cuerpo de respuesta sean textualmente idénticos en cada llamada. La idempotencia aplica al efecto secundario sobre el estado del sistema en el servidor, no al cuerpo de la respuesta devuelta.
4. Código de estado HTTP (Status Code)#
- Intuición cotidiana: El sello oficial estampado en la resolución de un trámite: "Aprobado con documento creado" (
201), "Documento con errores ilegibles" (400), "No encontrado en archivo" (404), "Conflicto con trámite previo" (409), o "Fallo de la maquinaria de la oficina" (500). - Definición técnica precisa: Entero decimal de 3 dígitos devuelto en la línea de estado (Status-line) que clasifica el resultado de la solicitud (RFC 9110, sección 15):
2xx(Successful): La solicitud fue recibida, entendida y aceptada exitosamente (200 OK,201 Created,204 No Content).4xx(Client Error): La solicitud contiene un error atribuible al cliente; el cliente no debe repetir la solicitud exactamente igual sin modificarla (400 Bad Request,404 Not Found,409 Conflict,422 Unprocessable Content).5xx(Server Error): El servidor falló al procesar una solicitud aparentemente válida debido a un error no recuperado en su propio código, base de datos o infraestructura (500 Internal Server Error,502 Bad Gateway,503 Service Unavailable).- Ejemplo en el cable:
HTTP/1.1 201 Created
Location: /tasks/105
- Concepción errónea: Responder siempre
200 OKy colocar un campo{ "status": "error", "code": 404 }dentro del cuerpo JSON ("Soft 200"). Esta práctica rompe cachés HTTP, proxies inversos, balanceadores de carga, métricas de observabilidad y clientes automatizados.
5. Cabeceras (Headers: Content-Type, Location, Idempotency-Key)#
- Intuición cotidiana: Metadatos sobre el sobre de envío: qué idioma/formato tiene el papel adentro (
Content-Type), dónde puedes encontrar el nuevo recurso tramitado (Location), y el código único de seguimiento para evitar pagos duplicados (Idempotency-Key). - Definición técnica precisa: Pares campo-valor delimitados por dos puntos que configuran el contexto semántico de la transmisión:
Content-Type: Cabecera de representación que especifica el tipo MIME (Media Type) del cuerpo (ej.application/json; charset=utf-8).Location: En respuestas201 Created, indica el URI canónico absoluto o relativo donde reside el recurso recién creado. En respuestas3xx, indica el destino de redirección.Idempotency-Key: Cabecera personalizada o estandarizada (IETF Draftdraft-ietf-httpapi-idempotency-key-header) enviada por el cliente con un identificador único global (UUID v4) para convertir un método no idempotente (POST) en una operación segura ante reintentos de red.- Ejemplo en código:
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
- Concepción errónea: Pensar que añadir la cabecera
Idempotency-Keyen el cliente garantiza mágicamente la idempotencia sin que el servidor y su base de datos implementen tablas de bloqueo y deduplicación.
6. Serialización / Deserialización (Parseo)#
- Intuición cotidiana: Serializar es empacar un mueble de madera desarmándolo en piezas planas y metiéndolo en una caja con etiquetas. Deserializar es abrir la caja, leer el instructivo y reconstruir el mueble en tres dimensiones en tu sala.
- Definición técnica precisa:
- Serialización: Transformación de una estructura de datos en memoria (un struct de Rust) a una secuencia de bytes transmitible (JSON, MessagePack, Protobuf).
- Deserialización (Parseo): Reconstrucción de un tipo de datos estructurado en memoria a partir de una secuencia de bytes crudos, validando que cumpla con la gramática formal del formato de intercambio.
- Ejemplo en Rust conceptual:
// Bytes JSON en el cable -> Deserialización -> Instancia tipada en RAM
// "{\"title\":\"Rust\"}" -> serde_json::from_slice() -> TaskRequest { title: "Rust" }
- Concepción errónea: Suponer que si un JSON deserializa exitosamente, los datos son válidos para el negocio. Deserializar solo garantiza validez sintáctica, no validez semántica.
7. Validación sintáctica vs Validación semántica de dominio#
- Intuición cotidiana: Si llenas un formulario de registro civil:
- Sintaxis: El campo "edad" contiene caracteres numéricos válidos (
"300"es sintácticamente un número entero válido). - Semántica de dominio: Ningún ser humano vivo tiene 300 años de edad. El valor es sintácticamente correcto pero semánticamente absurdo e inválido para el dominio.
- Definición técnica precisa:
- Validación sintáctica: Comprobación de que la carga útil obedece las reglas estructurales del formato de transporte (JSON bien formado, campos con los tipos escalares esperados: enteros como números, cadenas entre comillas, etc.). Ocurre típicamente en la capa de extracción/transporte HTTP (
400 Bad Requesto415 Unsupported Media Type). - Validación semántica de dominio: Comprobación de que los valores estructurados respetan los invariantes del modelo de negocio (título no compuesto únicamente por espacios en blanco, montos positivos, fechas de expiración en el futuro, identificadores referenciales válidos). Ocurre en el núcleo de dominio (
422 Unprocessable Contento400 Bad Requestcon esquema de error detallado). - Ejemplo en código:
{"title": " "}: Sintácticamente es un objeto JSON perfecto con un campo title de tipo string. Semánticamente, viola el invariante de dominio "el título de una tarea no puede estar vacío tras eliminar espacios en blanco".
- Concepción errónea: Delegar las reglas de dominio al deserializador JSON (por ejemplo, apoyarse únicamente en anotaciones sintácticas del serializador y omitir los tipos de dominio con invariantes).
8. Clave de idempotencia y huella (Fingerprint) del payload#
- Intuición cotidiana: Si compras un boleto de avión, recibes un número de reserva único. Si la pantalla se congela y vuelves a pulsar "Pagar" con el mismo número de reserva y los mismos datos de pasajero, el sistema te muestra el boleto ya emitido sin cobrarte dos veces. Pero si usas el mismo número de reserva para comprar un boleto a otro destino, el sistema rechaza la operación por inconsistencia.
- Definición técnica precisa: Mecanismo en el que el cliente envía un identificador único (clave de idempotencia) junto a la solicitud. El servidor genera una huella criptográfica unidireccional (hash SHA-256) del cuerpo y parámetros de la solicitud (payload fingerprint). Al recibir la petición, el servidor consulta atómicamente la clave en almacenamiento persistente:
- Si la clave no existe, guarda la clave, la huella y marca el estado como
IN_PROGRESS. - Si la clave ya existe con la misma huella y estado
COMPLETED, devuelve la respuesta almacenada previamente sin reejecutar la lógica. - Si la clave existe pero la huella es diferente, rechaza con
409 Conflicto422 Unprocessable Contentporque se intentó reutilizar una clave para dos operaciones semánticamente distintas. - Ejemplo en pseudocódigo:
request_hash = SHA256(request_body + request_path)
stored_entry = DB.get(idempotency_key)
if stored_entry.hash != request_hash { return 409_Conflict }
- Concepción errónea: Almacenar solo la clave de idempotencia sin comparar la huella del cuerpo. Si un cliente reusa por error la misma clave para crear dos tareas distintas, el segundo recurso se perdería silenciosamente y se devolvería el primero.
9. Fuga de detalles internos (Information Leakage / CWE-209)#
- Intuición cotidiana: Si vas a un banco y el cajero automático falla, la pantalla dice: "No es posible procesar su transacción en este momento, intente más tarde". La pantalla jamás debe imprimir el volcado de memoria del microprocesador ni la contraseña del servidor central de la bóveda.
- Definición técnica precisa: Debilidad de seguridad (CWE-209: Generation of Error Message Containing Sensitive Information) donde el servidor expone a clientes externos detalles de su implementación: trazas de pila (stack traces), sintaxis de sentencias SQL fallidas, nombres de tablas/columnas, versiones exactas de bibliotecas o rutas absolutas del sistema de archivos local.
- Ejemplo de respuesta vulnerable:
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
DatabaseError: syntax error at or near "FROM" at line 1 in /app/src/db.rs:84
- Ejemplo de respuesta segura:
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{"type":"about:blank","title":"Internal Server Error","status":500,"trace_id":"req-987123"}
- Concepción errónea: Pensar que ocultar errores dificulta la depuración. Los detalles técnicos completos deben enviarse a un sistema de registro de eventos (structured logs/tracing) interno correlacionados con un
trace_idanónimo devuelto al cliente.
10. Prueba de contrato (Contract Test)#
- Intuición cotidiana: Una prueba de encastre: verificar con un calibrador que el enchufe mide exactamente 4 mm de diámetro y 19 mm de separación, sin necesidad de encender la planta hidroeléctrica que produce la electricidad.
- Definición técnica precisa: Prueba automatizada que valida el acuerdo de comunicación observable entre dos límites del sistema (cliente y servidor). Verifica que ante entradas específicas de transporte (método, ruta, cabeceras, cuerpo), el sistema produzca exactamente los códigos de estado, cabeceras obligatorias y esquemas de respuesta estipulados en la especificación formal (OpenAPI/RFC), independientemente de las optimizaciones internas del código.
- Ejemplo en aserción:
assert_eq!(response.status(), 400); y assert_eq!(response.headers().get("Content-Type"), "application/problem+json");.
- Concepción errónea: Creer que una prueba unitaria de una función interna reemplaza una prueba de contrato. La función interna puede funcionar perfectamente de forma aislada, pero si el handler HTTP mapea mal el error y devuelve
200o no fija elContent-Type, el contrato externo se rompe.
Modelo mental y arquitectura profunda#
Las 6 etapas en el procesamiento de una solicitud HTTP en el servidor#
Cuando un cliente emite una petición web, el mensaje atraviesa una serie de capas de abstracción estrictas. Si una etapa detecta una violación de contrato, la solicitud debe abortarse inmediatamente en ese punto exacto devolviendo el código HTTP preciso que identifica el nivel de la falla:
sequenceDiagram
autonumber
participant Client as Cliente HTTP
participant OS as Kernel / Socket TCP
participant Router as Enrutador HTTP
participant Reader as Lector de Bytes (Body)
participant Parser as Deserializador Sintáctico
participant Domain as Validador de Dominio
participant DB as Persistencia / Estado
Client->>OS: Conexión TCP (SYN, SYN-ACK, ACK)
Note over OS: Etapa 1: Capa de Transporte
OS-->>Client: Error si saturado (ECONNRESET / ETIMEDOUT)
OS->>Router: Petición HTTP cruda (Start-line + Headers)
Note over Router: Etapa 2: Resolución de Ruta y Verbo
Router-->>Client: 404 Not Found (Ruta inexistente)<br/>405 Method Not Allowed (Verbo no soportado)
Router->>Reader: Lectura de stream del cuerpo (Payload)
Note over Reader: Etapa 3: Recepción de Bytes
Reader-->>Client: 408 Request Timeout (Lectura lenta)<br/>413 Payload Too Large (Excede límite)
Reader->>Parser: Bytes crudos -> Struct tipado (JSON)
Note over Parser: Etapa 4: Análisis Sintáctico
Parser-->>Client: 415 Unsupported Media Type (Cabecera errónea)<br/>400 Bad Request (JSON roto o tipos incompatibles)
Parser->>Domain: DTO tipado -> Reglas de Negocio
Note over Domain: Etapa 5: Invariantes Semánticas
Domain-->>Client: 422 Unprocessable Content (Violación de reglas)
Domain->>DB: Comando validado -> Transacción ACID
Note over DB: Etapa 6: Persistencia y Deduplicación
DB-->>Domain: Conflicto o Error de Base de Datos
Domain-->>Client: 409 Conflict (Idempotencia o Unicidad)<br/>500 Internal Server Error (Fallo inesperado DB)
DB->>Client: 201 Created + Location (Éxito definitivo)
- Etapa 1: Socket TCP (Aceptación a nivel de Kernel/SO):
- Qué ocurre: El sistema operativo negocia el protocolo de tres vías de TCP (three-way handshake). La conexión pasa de
LISTENaESTABLISHED. - Fallo observable: Si el servidor está saturado o el puerto cerrado, el cliente experimentaECONNREFUSEDoETIMEDOUT. Aquí aún no existe el protocolo HTTP; no se emite ningún código4xxni5xx. - *Etapa 2: Enrutamiento (Routing de método y URI):
- Qué ocurre: El servidor lee la Start-line e inspecciona el verbo y la ruta solicitada.
- Punto de falla:* Si la ruta no está registrada en la tabla de enrutamiento, devuelve
404 Not Found. Si la ruta existe (por ejemploGET /tasks) pero se invocó con un método no configurado (comoPURGE /tasks), devuelve405 Method Not Allowedacompañado de la cabecera obligatoriaAllow: GET, POST. - *Etapa 3: Lectura de bytes del cuerpo (Stream reading):
- Qué ocurre: El servidor drena los bytes que viajan a través del socket según la longitud declarada en
Content-Length. - Punto de falla: Si el cliente envía bytes a una tasa inferior al umbral de mantenimiento, devuelve408 Request Timeout. Si el cliente intenta transmitir un cuerpo de 50 MB en un endpoint configurado con límite de 2 MB, el servidor aborta la lectura y emite413 Payload Too Largepara prevenir ataques de denegación de servicio (DoS* por agotamiento de memoria). - *Etapa 4: Deserialización sintáctica (Syntactic parsing):
- Qué ocurre: El framework convierte el buffer de bytes a tipos estructurales (por ejemplo, invocando
serde_json). - Punto de falla:* Si la cabeceraContent-Typeno coincide con el parser esperado (ej. envíatext/plaina un endpoint JSON), devuelve415 Unsupported Media Type. Si el cuerpo tiene llaves abiertas sin cerrar ({"title": "Rust), o si un campo numérico recibe una cadena ({"priority": "alta"}cuando se esperaba un enterou32), la deserialización falla antes de llamar a tu lógica de negocio y emite400 Bad Request. - Etapa 5: Validación semántica e invariantes de dominio:
- Qué ocurre: El DTO sintácticamente válido se entrega a las funciones o constructores del dominio (ej.
TaskTitle::parse(raw_title)). - Punto de falla: Si la cadena está compuesta enteramente de espacios en blanco (" "), o si una fecha de vencimiento corresponde al año 1970, la sintaxis JSON es impecable, pero la semántica es inaceptable. El estándar RFC 9110 designa para este caso el código422 Unprocessable Content(históricamente introducido en WebDAV y hoy formalizado para HTTP en general), aunque múltiples APIs públicas adoptan400 Bad Requestcon un cuerpo estructurado que detalla los campos defectuosos. - Etapa 6: Persistencia, deduplicación y traducción de respuesta:
- Qué ocurre: La operación válida ejecuta su transacción en base de datos.
- Puntos de falla y éxito:
- Si se viola una restricción de clave foránea o unicidad concurrente, se traduce a
409 Conflict. - Si la base de datos pierde la conexión o el disco se llena, se captura internamente y se emite500 Internal Server Error(ocultando el stack trace). - Si se crea un recurso nuevo, se emite201 Createdcon la cabeceraLocation: /tasks/105. - Si la operación tiene éxito pero el contrato estipula que no se transfiere ninguna carga útil de retorno, se emite204 No Content.
Tabla comparativa de métodos HTTP según RFC 9110#
| Método HTTP | ¿Seguro (Safe)? | ¿Idempotente por estándar? | Semántica prevista | Códigos de éxito típicos | Códigos de error cliente típicos |
|---|---|---|---|---|---|
GET | Sí | Sí | Recuperar la representación actual del recurso identificado. No debe mutar el estado del servidor. | 200 OK | 400, 401, 403, 404 |
HEAD | Sí | Sí | Idéntico a GET, pero el servidor jamás debe transferir el cuerpo en la respuesta. Usado para consultar cabeceras y tamaño. | 200 OK | 404 |
POST | No | No | Procesar la carga útil según la semántica propia del recurso (típicamente creación, envíos subordinados, comandos). | 201 Created, 200 OK, 204 No Content | 400, 409, 415, 422 |
PUT | No | Sí | Reemplazar completamente el estado del recurso destino con el estado transmitido en el cuerpo. | 200 OK, 204 No Content, 201 Created | 400, 404, 409, 422 |
PATCH | No | No (por defecto) | Aplicar una modificación parcial al recurso según un conjunto de instrucciones (RFC 5789). Puede ser diseñado como idempotente, pero el estándar no lo presupone. | 200 OK, 204 No Content | 400, 404, 409, 422 |
DELETE | No | Sí | Eliminar la asociación del recurso identificado. Múltiples ejecuciones dejan el recurso inexistente. | 204 No Content, 200 OK | 404 (o 204), 409 |
OPTIONS | Sí | Sí | Consultar las capacidades de comunicación y métodos permitidos para un recurso o servidor. | 200 OK, 204 No Content | 404 |
Mecánica de una clave de idempotencia a nivel de base de datos#
Un error común en principiantes es asumir que añadir una cabecera Idempotency-Key en la solicitud otorga automáticamente inmunidad ante duplicados. La red entre el cliente y el servidor es inherentemente falible (el teorema de la imposibilidad de consenso en redes asíncronas con fallos):
sequenceDiagram
participant C as Cliente
participant H as Servidor HTTP
participant DB as Base de Datos
C->>H: POST /tasks (Idempotency-Key: K1, Payload: P1)
H->>DB: INSERT INTO idempotency_records (key=K1, hash=H(P1), status='IN_PROGRESS')
Note over DB: Bloqueo atómico por PRIMARY KEY
H->>DB: INSERT INTO tasks (...) -> ID: 42
H->>DB: UPDATE idempotency_records SET status='COMPLETED', response_code=201, response_body='{"id":42...}'
H-->>C: [Fallo de Red: El paquete de respuesta se pierde en el cable]
Note over C: El cliente sufre Timeout y reintenta
C->>H: POST /tasks (Idempotency-Key: K1, Payload: P1) [REINTENTO]
H->>DB: SELECT status, hash, response_code, response_body FROM idempotency_records WHERE key=K1
DB-->>H: Record encontrado: status='COMPLETED', hash coincide
Note over H: No se ejecuta dominio ni inserción de tarea
H-->>C: HTTP 201 Created (Copia idéntica de la respuesta original: {"id":42...})
Anatomía del registro de idempotencia en base de datos:#
Para implementar este patrón con solidez militar se requiere una tabla dedicada con control transaccional estricto:
-- DDL ilustrativo para almacenamiento de idempotencia
CREATE TABLE idempotency_records (
key UUID PRIMARY KEY,
request_hash BYTEA NOT NULL, -- SHA-256 del cuerpo y parámetros
status TEXT NOT NULL CHECK (status IN ('IN_PROGRESS', 'COMPLETED')),
response_code INT NULL,
response_headers JSONB NULL,
response_body TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL -- Ventana de retención (TTL, ej. 24 horas)
);
Flujo de validación ante colisión de claves:#
- Misma clave y misma huella (
status = COMPLETED): El cliente experimentó una pérdida de paquete en el reintento. El servidor localiza la fila, extraeresponse_code,response_headersyresponse_bodyy los devuelve directamente sin tocar el dominio ni insertar filas adicionales en la tabla de tareas. - Misma clave y misma huella (
status = IN_PROGRESS): Dos solicitudes idénticas llegaron casi en el mismo milisegundo (por ejemplo, doble clic del usuario). La transacción de la primera sigue en ejecución. La segunda solicitud debe ser rechazada con409 Conflict(o425 Too Early), indicando en una cabeceraRetry-After: 1que hay una operación en curso para esa clave. - *Misma clave y distinta huella (Payload mismatch): El cliente reutilizó el UUID
K1pero alteró el cuerpo (por ejemplo, cambió{"title": "Pagar nómina"}por{"title": "Comprar insumos"}). Esto representa una violación crítica de contrato. El servidor debe rechazar inmediatamente la solicitud con409 Conflicto422 Unprocessable Content*, impidiendo que una mutación no deseada adopte el resultado de otra operación. - Expiración de clave (TTL): Las claves no pueden guardarse indefinidamente. Tras vencer el TTL (ej. 24 a 48 horas), una tarea de limpieza elimina las filas caducadas. Si un cliente reintenta una semana después con la misma clave, el servidor la procesará como una nueva solicitud.
Diagrama#
sequenceDiagram
participant C as Cliente
participant H as Handler HTTP
participant D as Dominio
participant R as Persistencia
C->>H: Método, ruta, cabeceras, cuerpo
H->>H: Analizar y validar formato
H->>D: Comando válido
D->>R: Consultar o cambiar estado
R-->>D: Resultado o error
D-->>H: Resultado de dominio
H-->>C: Estado, cabeceras y cuerpo
El diagrama anterior modela la separación de fronteras: el handler coordina el transporte; el dominio gobierna las reglas; la persistencia asegura la durabilidad.
Ejemplo razonado#
Analicemos la anatomía en el cable de una solicitud de creación de tarea y sus dos posibles resoluciones: éxito con cabecera de ubicación y fallo semántico estructurado.
Solicitud en el cable (Raw HTTP Request)#
POST /tasks HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: 7b312e09-0d12-4f77-9a0e-bc438e785e29
Content-Length: 27
{"title":"Audit contracts"}
Observa la línea en blanco obligatoria (\r\n\r\n) entre la cabecera Content-Length y el cuerpo JSON.
Caso de éxito: 201 Created con cabecera Location#
HTTP/1.1 201 Created
Date: Sun, 18 Oct 2026 20:15:00 GMT
Location: /tasks/105
Content-Type: application/json; charset=utf-8
Content-Length: 46
{"id":105,"title":"Audit contracts","done":false}
Por qué este diseño es robusto:
- El código
201 Createdinforma formalmente al cliente que un nuevo recurso duradero ha cobrado existencia. - La cabecera
Location: /tasks/105entrega el URI canónico para que clientes REST o navegadores puedan consultar o enlazar el recurso sin tener que adivinar la estructura de rutas del backend.
Caso de error semántico: 422 Unprocessable Content (RFC 9457 / Problem Details)#
Si el cliente envía {"title":" "} (sintácticamente válido, semánticamente vacío), el servidor no debe devolver texto plano ni una página HTML genérica:
HTTP/1.1 422 Unprocessable Content
Date: Sun, 18 Oct 2026 20:15:01 GMT
Content-Type: application/problem+json; charset=utf-8
Content-Length: 174
{
"type": "https://api.example.com/errors/empty-task-title",
"title": "Unprocessable Content",
"status": 422,
"detail": "Task title cannot be empty or contain only whitespace.",
"instance": "/tasks"
}
Por qué este diseño es robusto:
- Cumple con el estándar RFC 9457 (Problem Details for HTTP APIs).
- El cliente tiene un código de estado de nivel de protocolo (
422), un identificador de tipo de error legible por máquina (type), una descripción humana no confidencial (detail) y el endpoint afectado (instance). - No revela consultas SQL, tablas internas, rutas de archivos del servidor ni stack traces del lenguaje.
Práctica independiente#
Diseña formalmente el contrato del endpoint POST /tasks. La API acepta un título obligatorio y una fecha de vencimiento opcional en formato ISO 8601 (due_date, ej. "2026-12-31T23:59:59Z").
Matriz de contratos obligatoria:#
Antes de escribir cualquier código de implementación, completa en tu cuaderno de notas o archivo de diseño una matriz formal de 8 escenarios:
| # | Escenario de entrada | Cabecera Content-Type | Cuerpo de la solicitud | Código HTTP esperado | Cabeceras clave en respuesta | Razón y justificación de arquitectura |
|---|---|---|---|---|---|---|
| 1 | Sintaxis corrupta | application/json | {"title": "Broken (incompleto) | 400 Bad Request | Content-Type: application/problem+json | Falla en deserialización sintáctica; el JSON no se puede parsear. |
| 2 | Tipo de medio no soportado | text/xml | <task><title>XML</title></task> | 415 Unsupported Media Type | Accept: application/json | El servidor rechaza representaciones que no sean JSON antes de leer el cuerpo. |
| 3 | Carga útil excesiva | application/json | {"title": "<10 megabytes de texto>"} | 413 Payload Too Large | Connection: close | Protección contra agotamiento de memoria en el socket de lectura. |
| 4 | Campo requerido ausente | application/json | {"due_date": "2026-12-31T23:59:59Z"} | 422 (o 400) | Content-Type: application/problem+json | Deserializa pero falta el invariante del campo requerido title. |
| 5 | Violación de regla de dominio | application/json | {"title": " "} | 422 Unprocessable Content | Content-Type: application/problem+json | Título vacío tras aplicar trim. La sintaxis es válida; el dominio la rechaza. |
| 6 | Solicitud válida de creación | application/json | {"title": "Deploy API"} | 201 Created | Location: /tasks/101 | Invariantes satisfechas; recurso creado y URI expuesto. |
| 7 | Reintento idéntico idempotente | application/json | Clave UUID-1 + mismo payload | 201 Created | Location: /tasks/101 | Misma clave y misma huella: devuelve respuesta idéntica sin crear fila duplicada. |
| 8 | Conflicto de clave reutilizada | application/json | Clave UUID-1 + payload alterado | 409 Conflict | Content-Type: application/problem+json | La clave ya se usó para otra operación; se rechaza la mutación contradictoria. |
Una vez completada tu tabla de diseño, contrástala contra la guía de soluciones de backend.
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 falacia del código 200 OK con carga útil de error#
En un sistema heredado, el endpoint POST /checkout devuelve siempre 200 OK. Si la tarjeta no tiene fondos, el JSON retornado es {"success": false, "error": "insufficient_funds"}.
- ¿Qué sucede cuando este endpoint se coloca detrás de un balanceador de carga o un proxy inverso con reintentos automáticos ante códigos de error HTTP?
- ¿Cómo afecta esto a los sistemas de alertas y monitoreo (como Datadog o Prometheus) que calculan la tasa de disponibilidad del servicio basándose en códigos
5xxy4xx? - ¿Por qué el código de estado HTTP debe reflejar el resultado semántico del protocolo en lugar de esconderse dentro del formato del payload?
Pregunta 2: Análisis formal de idempotencia (f(f(x)) = f(x))#
Un desarrollador afirma: "Como DELETE /tasks/42 devuelve 204 No Content en la primera llamada y 404 Not Found en la segunda llamada, el método DELETE viola el principio de idempotencia porque devuelve respuestas distintas".
- Desmonta esta afirmación a partir de la definición de idempotencia de la sección 9.2.2 del RFC 9110.
- ¿La idempotencia mide la equivalencia textual de los paquetes de respuesta devueltos o el estado residual en el servidor tras la ejecución?
- Si
GET /tasks/42incrementa un contador interno de visitas en la base de datos cada vez que se lee, ¿continúa siendo un método seguro (Safe) bajo el estándar HTTP? ¿Por qué?
Pregunta 3: Ventana de colisión en comprobaciones de idempotencia#
Supón que el backend implementa idempotencia de esta manera:
// Código ingenuo
if db.exists(&idempotency_key) {
return db.get_saved_response(&idempotency_key);
}
let response = execute_order();
db.save_response(&idempotency_key, &response);
return response;
- Describe el escenario de carrera concurrente (TOCTOU - Time-Of-Check to Time-Of-Use) exacto en el que dos solicitudes con la misma clave
Kllegan simultáneamente al servidor en dos hilos diferentes. - ¿Por qué la inserción de la clave de idempotencia debe ser atómica y preliminar con estado
IN_PROGRESSantes de ejecutar la lógica de la orden?
Pregunta 4: Deserialización vs Tipos Refinados de Dominio#
Un DTO se declara como:
struct CreateTaskPayload {
pub title: String,
}
- Si un cliente malicioso envía un JSON con
titlecompuesto por un string de 20 megabytes o por una cadena que contiene caracteres de control no imprimibles (\0), ¿falla la deserialización sintáctica de JSON? - ¿Qué responsabilidad tiene la capa de dominio antes de aceptar el string y por qué envolver el dato en un tipo
TaskTitle(String)mediante un método constructorparse(raw: &str) -> Result<TaskTitle, DomainError>protege las capas internas de la aplicación?
Pregunta 5: La anatomía del delimitador \r\n\r\n#
- Explica qué función cumple a nivel de parser de flujo de red (stream parser) el delimitador de retorno de carro y salto de línea doble
\r\n\r\n(bytes0x0D 0x0A 0x0D 0x0A). - Si un cliente envía una solicitud HTTP con cabeceras pero olvida emitir el segundo
\r\n, ¿qué le ocurre al hilo del servidor que está esperando leer las cabeceras a través del socket TCP? ¿Qué ataque de denegación de servicio (Slowloris) explota precisamente esta debilidad?
Code smell#
Síntoma: Fuga de detalles de implementación en mensajes de error (CWE-209)#
Un endpoint responde a una consulta con fallo de la siguiente manera:
HTTP/1.1 500 Internal Server Error
Content-Type: text/html
<h1>Internal Server Error</h1>
<pre>
PostgresError: relation "secret_users_table" does not exist at character 19
Query: SELECT * FROM secret_users_table WHERE user_id = 'abc';
File: /srv/backend/src/persistence/user_repo.rs:142
</pre>
- Evidencia observable: Exposición pública de sintaxis de base de datos, nombres de tablas internas, rutas locales de archivos fuente y líneas de código.
- Hipótesis del problema: Los desarrolladores conectaron directamente el error devuelto por la biblioteca de base de datos (
sqlx::Errorodiesel::result::Error) a la respuesta HTTP para "facilitar la depuración durante el desarrollo local", olvidando implementar un middleware o mapeador de excepciones para producción. - Consecuencia concreta: Un atacante realiza reconocimiento activo del sistema: descubre qué motor de base de datos se utiliza, cómo están estructuradas las tablas, qué rutas del sistema operativo existen y qué debilidades de inyección pueden explotarse.
- Tratamiento mínimo justificado:
- Registrar el error técnico completo en los registros estructurados internos (logs) del servidor con nivel
ERROR, incluyendo el stack trace y los parámetros. - Generar un identificador opaco aleatorio (ej.
trace_id = Uuid::new_v4()). - Devolver al cliente una respuesta genérica normalizada bajo el estándar RFC 9457 (
application/problem+json) que contenga únicamente el código500, un mensaje no técnico ("An unexpected error occurred") y el campo"trace_id": "..."para que el usuario pueda reportarlo a soporte.
Criterio de salida#
Puedes considerar superada esta lección cuando:
- Puedes trazar paso a paso las 6 etapas del procesamiento de una solicitud HTTP y asociar el código de estado formal exacto a cada tipo de falla (sintaxis, medio, tamaño, ruta, semántica, persistencia).
- Tu tabla de contrato distingue con precisión al menos 8 resultados diferentes con su método, código de estado, cabeceras y cuerpo.
- Puedes explicar la diferencia entre un método seguro y uno idempotente, y demostrar matemáticamente por qué
DELETEes idempotente aunque sus respuestas devuelvan códigos numéricos distintos en sucesivas llamadas. - Eres capaz de diseñar el esquema de base de datos y la máquina de estados necesaria para garantizar idempotencia en operaciones de escritura (
POST). - Comprendes el riesgo de seguridad de la fuga de trazas de pila (CWE-209) y aplicas el estándar RFC 9457 para el reporte de fallos.
Lecturas oficiales#
- RFC 9110: HTTP Semantics: La especificación canónica internacional de métodos, cabeceras y códigos de estado.
- RFC 9457: Problem Details for HTTP APIs: Estándar oficial para la representación estructurada de errores en APIs HTTP.
- IETF Draft: Idempotency-Key HTTP Header Field: Especificación de la cabecera estándar de idempotencia.
- MDN: Visión general del protocolo HTTP: Guía conceptual completa sobre mensajes y ciclo de vida de la red.
- Glosario técnico del curso: Definiciones de referencia rápida para términos de arquitectura backend.
Siguiente paso#
Continúa con 06 · Actix: límites entre HTTP y dominio, donde transformarás este diseño de contratos en un endpoint real, separando estrictamente la capa de transporte web de las reglas puras de dominio en Rust.