HTTP: el protocolo base
En el restaurante de la sección 16, definimos la cocina (cómputo) y en 01_que_es_una_api.md introdujimos al mesero (la API). Ahora definimos el idioma que hablan mesero y cocina entre sí. Ese idioma es HTTP — Hypertext Transfer Protocol.
HTTP es el protocolo de comunicación más importante de la web. Todo lo que veremos en este módulo — REST, SSE, WebSocket, GraphQL — se construye sobre HTTP o empieza con un handshake HTTP. Entenderlo bien es entender los cimientos.
Anatomía de una petición HTTP
Cada vez que tu chatbot llama al LLM, viaja un request HTTP por la red. Veamos su estructura exacta usando una llamada real a la API de Anthropic:
┌─────────────────────────────────────────────────────────────────┐
│ HTTP REQUEST │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ LÍNEA DE PETICIÓN (request line) │ │
│ │ │ │
│ │ POST /v1/messages HTTP/1.1 │ │
│ │ ─┬── ─────┬────── ──┬─── │ │
│ │ │ │ │ │ │
│ │ │ │ └── versión del protocolo │ │
│ │ │ └── ruta del recurso (endpoint) │ │
│ │ └── método (qué quieres hacer) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ HEADERS (metadatos de la petición) │ │
│ │ │ │
│ │ Host: api.anthropic.com │ │
│ │ Content-Type: application/json │ │
│ │ x-api-key: sk-ant-api03-xxxxx │ │
│ │ anthropic-version: 2023-06-01 │ │
│ │ Accept: application/json │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ── línea en blanco (separa headers del body) ── │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ BODY (datos que envías) │ │
│ │ │ │
│ │ { │ │
│ │ "model": "claude-sonnet-4-20250514", │ │
│ │ "max_tokens": 1024, │ │
│ │ "messages": [ │ │
│ │ { │ │
│ │ "role": "user", │ │
│ │ "content": "¿Qué es una API?" │ │
│ │ } │ │
│ │ ] │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Tres partes, siempre en este orden:
- Línea de petición — método + ruta + versión
- Headers — metadatos clave-valor
- Body — datos (opcional, no todos los métodos lo usan)
¿Qué significa cada header del ejemplo?
Host: api.anthropic.com ← A qué servidor va la petición.
Un mismo IP puede servir múltiples
dominios; Host le dice cuál quieres.
Content-Type: application/json ← "Los datos que te envío están en
formato JSON." Sin esto, el servidor
no sabe cómo interpretar el body.
x-api-key: sk-ant-api03-xxxxx ← Tu clave de autenticación. Es como
tu número de reservación: sin ella,
el servidor te responde 401.
El prefijo "x-" indica header custom
(no estándar de HTTP).
anthropic-version: 2023-06-01 ← Versión de la API que quieres usar.
Las APIs evolucionan; este header
garantiza que tu código no se rompa
cuando el proveedor cambia el formato
de respuesta.
Accept: application/json ← "Quiero que me respondas en JSON."
Es la contraparte de Content-Type:
uno dice qué formato ENVÍAS,
el otro qué formato QUIERES RECIBIR.
En el restaurante: los headers son las instrucciones que le das al mesero además de tu orden. “Soy vegetariano” (Accept), “aquí está mi reservación” (x-api-key), “quiero el menú de primavera” (anthropic-version).
Anatomía de una respuesta HTTP
El servidor procesa tu petición y devuelve una response:
┌─────────────────────────────────────────────────────────────────┐
│ HTTP RESPONSE │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ LÍNEA DE ESTADO (status line) │ │
│ │ │ │
│ │ HTTP/1.1 200 OK │ │
│ │ ──┬───── ─┬─ ─┬ │ │
│ │ │ │ │ │ │
│ │ │ │ └── frase descriptiva (para humanos) │ │
│ │ │ └── código de estado (para máquinas) │ │
│ │ └── versión del protocolo │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ HEADERS │ │
│ │ │ │
│ │ Content-Type: application/json │ │
│ │ x-request-id: req_01ABC... │ │
│ │ x-ratelimit-limit: 1000 │ │
│ │ x-ratelimit-remaining: 999 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ── línea en blanco ── │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ BODY │ │
│ │ │ │
│ │ { │ │
│ │ "id": "msg_01XFDUDYJgAACzvnptvVoYEL", │ │
│ │ "type": "message", │ │
│ │ "role": "assistant", │ │
│ │ "content": [ │ │
│ │ { │ │
│ │ "type": "text", │ │
│ │ "text": "Una API es..." │ │
│ │ } │ │
│ │ ], │ │
│ │ "model": "claude-sonnet-4-20250514", │ │
│ │ "usage": { │ │
│ │ "input_tokens": 14, │ │
│ │ "output_tokens": 52 │ │
│ │ } │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Métodos HTTP
HTTP define un conjunto de métodos (también llamados verbos) que indican la intención de la petición. Cada método tiene una semántica clara:
| Método | En el restaurante | En APIs | Ejemplo LLM | Body |
|---|---|---|---|---|
| GET | “¿Qué platillos tienen hoy?” | Leer un recurso | Obtener lista de modelos | No |
| POST | “Quiero ordenar este platillo” | Crear un recurso nuevo | Enviar mensaje al LLM | Si |
| PUT | “Cambie toda mi orden por esta otra” | Reemplazar un recurso completo | Actualizar configuración de assistant | Si |
| PATCH | “Agregue salsa extra a mi orden” | Modificar parte de un recurso | Cambiar nombre de un fine-tune | Si |
| DELETE | “Cancele mi orden” | Eliminar un recurso | Borrar una API key | No |
Propiedades de los métodos
Idempotente Seguro
(repetir no (no modifica
cambia nada) el estado)
┌───────────┐ ┌───────────┐
GET ────────────│ SI │───────│ SI │
POST ───────────│ NO │───────│ NO │
PUT ────────────│ SI │───────│ NO │
PATCH ──────────│ NO* │───────│ NO │
DELETE ─────────│ SI │───────│ NO │
└───────────┘ └───────────┘
* PATCH puede ser idempotente dependiendo de la implementación
Idempotente significa que hacer la misma petición 1 vez o 100 veces produce el mismo resultado. GET /models siempre devuelve la misma lista. DELETE /key/123 borra la key la primera vez; las siguientes no cambian nada (ya está borrada). Pero POST /v1/messages envía un mensaje nuevo cada vez.
Códigos de estado HTTP
El código de estado es un número de tres digitos que indica el resultado de la petición. Los primeros digitos definen la categoría:
1xx ── Informacional (raro en APIs)
2xx ── Exito
3xx ── Redirección
4xx ── Error del cliente (TU culpa)
5xx ── Error del servidor (SU culpa)
Los que verás constantemente
| Código | Significado | En el restaurante | Cuándo lo ves en APIs LLM |
|---|---|---|---|
| 200 OK | Todo bien, aquí está tu respuesta | “Aqui tiene su plato, buen provecho” | Respuesta exitosa del LLM |
| 201 Created | Se creó un recurso nuevo | “Nueva orden registrada, en un momento sale” | Fine-tune job creado |
| 204 No Content | Hecho, pero no hay nada que devolver | “Listo, su orden fue cancelada” | API key eliminada |
| 400 Bad Request | Tu petición no tiene sentido | “No entiendo su pedido — ¿‘pasta con helado de atun’?” | JSON malformado, campo faltante |
| 401 Unauthorized | No te identificaste | “No tiene reservación, no puedo dejarlo pasar” | Falta API key o es inválida |
| 403 Forbidden | Te identificaste pero no tienes permiso | “Tiene reservación pero no para la zona VIP” | API key válida pero sin permiso para ese modelo |
| 404 Not Found | Ese recurso no existe | “Ese platillo no existe en nuestro menú” | Endpoint incorrecto, modelo inexistente |
| 405 Method Not Allowed | El recurso existe pero no acepta ese método | “No puede cancelar un plato que ya se sirvió” | GET a un endpoint que solo acepta POST |
| 413 Payload Too Large | Enviaste demasiados datos | “Esa orden es demasiado grande, no cabe en la comanda” | Contexto excede el limite de tokens |
| 422 Unprocessable Entity | La sintaxis está bien pero la semántica no | “Entiendo las palabras pero no se puede: ‘filete término -3’” | max_tokens: -1, model: "no-existe" |
| 429 Too Many Requests | Estás pidiendo demasiado rápido | “Está pidiendo demasiado rápido, espere un momento” | Rate limit — muy común con APIs de LLM |
| 500 Internal Server Error | Algo se rompió en el servidor | “Se cayó la cocina, disculpe” | Bug en el servidor del proveedor |
| 502 Bad Gateway | El proxy/gateway recibió basura del servidor real | “El intercomunicador con la cocina falla” | Load balancer no puede contactar el backend |
| 503 Service Unavailable | Servidor sobrecargado o en mantenimiento | “Estamos llenos, no aceptamos más clientes” | Servidor del LLM sobrecargado |
El código que nunca esperarías
HTTP tiene un easter egg oficial: el código 418 I’m a teapot. Fue definido en el RFC 2324 como parte del Hyper Text Coffee Pot Control Protocol (HTCPCP) — una broma del April Fools de 1998.
418 I'm a teapot
"Me pides que prepare café, pero soy una tetera."
El servidor se rehúsa a preparar café porque es,
permanente y orgullosamente, una tetera.
No es solo una curiosidad — el 418 se usa en la práctica como respuesta genérica de “no voy a hacer eso” en servicios que quieren rechazar peticiones sin dar información específica (anti-bot, honeypots). Google lo devuelve en google.com/teapot.
Cómo manejar errores en Python
import requests
response = requests.post(
"https://api.anthropic.com/v1/messages",
headers={"x-api-key": API_KEY, "content-type": "application/json",
"anthropic-version": "2023-06-01"},
json={"model": "claude-sonnet-4-20250514", "max_tokens": 1024,
"messages": [{"role": "user", "content": "Hola"}]}
)
if response.status_code == 200:
data = response.json()
print(data["content"][0]["text"])
elif response.status_code == 429:
retry_after = response.headers.get("retry-after", 60)
print(f"Rate limit. Reintenta en {retry_after}s")
elif response.status_code == 401:
print("API key inválida")
else:
print(f"Error {response.status_code}: {response.text}")
Anatomía de una URL
Cada petición HTTP se dirige a una URL. Entender sus partes es fundamental:
https://api.anthropic.com:443/v1/messages?model=claude-sonnet-4-20250514#usage
──┬── ───────┬───────── ─┬─ ─────┬──── ──────────┬─────────── ──┬──
│ │ │ │ │ │
scheme host port path query fragment
│ │ │ │ │ │
│ │ │ │ │ └─ ancla local
│ │ │ │ │ (no se envía
│ │ │ │ │ al servidor)
│ │ │ │ │
│ │ │ │ └─ parámetros clave=valor
│ │ │ │ separados por &
│ │ │ │
│ │ │ └─ ruta al recurso
│ │ │ (como un path en el filesystem)
│ │ │
│ │ └─ puerto (443 = HTTPS, 80 = HTTP)
│ │ se omite cuando es el default
│ │
│ └─ servidor destino
│ (se resuelve a IP via DNS)
│
└─ protocolo de transporte
http = sin encriptar (NUNCA para APIs con datos sensibles)
https = encriptado con TLS
Ejemplos reales
GET https://api.anthropic.com/v1/models
└── listar modelos disponibles
POST https://api.anthropic.com/v1/messages
└── enviar mensaje al LLM
GET https://api.anthropic.com/v1/messages/msg_01ABC
└── obtener un mensaje específico por ID
POST https://api.openai.com/v1/fine_tuning/jobs
└── crear un job de fine-tuning
GET https://api.openai.com/v1/fine_tuning/jobs/ft-123/events?limit=10
└── obtener los últimos 10 eventos del job ft-123
Headers: los metadatos de la comunicación
Los headers son pares clave-valor que acompañan a cada request y response. No son los datos en sí, sino metadatos sobre la comunicación:
| Header | Dirección | En el restaurante | Ejemplo |
|---|---|---|---|
Content-Type |
Request y Response | “Esta orden está escrita en español” | application/json |
Accept |
Request | “Quiero la respuesta en español, no en francés” | application/json |
Authorization |
Request | “Aquí está mi reservación” | Bearer sk-... |
x-api-key |
Request | “Mi número de cliente frecuente” | sk-ant-api03-... |
anthropic-version |
Request | “Quiero el menú de primavera 2023” | 2023-06-01 |
Content-Length |
Request y Response | “Mi orden tiene 3 renglones” | 256 (bytes) |
x-ratelimit-remaining |
Response | “Le quedan 5 órdenes más esta hora” | 999 |
retry-after |
Response | “Vuelva en 30 segundos” | 30 |
x-request-id |
Response | “Su número de ticket es 4827” | req_01ABC... |
Headers en Python con requests
headers = {
"Content-Type": "application/json",
"x-api-key": "sk-ant-api03-xxxxx",
"anthropic-version": "2023-06-01",
"Accept": "application/json",
}
response = requests.post(url, headers=headers, json=body)
# Inspeccionar headers de la respuesta
print(response.headers["content-type"])
print(response.headers.get("x-ratelimit-remaining"))
print(response.headers.get("x-request-id"))
HTTPS y TLS: por qué importa la encriptación
HTTP transmite todo en texto plano. Cualquier nodo intermedio (tu ISP, el WiFi del café, un proxy corporativo) puede leer tus peticiones — incluyendo tu API key.
HTTPS = HTTP + TLS (Transport Layer Security). TLS encripta toda la comunicación entre cliente y servidor:
Sin TLS (HTTP):
Tu código ────── "x-api-key: sk-ant-..." ──────▶ Servidor
│
┌────┴────┐
│ ISP, │
│ WiFi, │ ◀── puede leer TODO
│ proxy │
└─────────┘
Con TLS (HTTPS):
Tu código ────── [datos encriptados] ──────▶ Servidor
│
┌────┴────┐
│ ISP, │
│ WiFi, │ ◀── ve basura ilegible
│ proxy │
└─────────┘
Regla simple: si la URL empieza con http:// y estás enviando API keys o datos sensibles, algo está mal. Todas las APIs de LLM usan HTTPS exclusivamente.
No necesitas entender los detalles criptográficos (cifrados, certificados, cadenas de confianza). Lo que necesitas saber es:
- HTTPS encripta la comunicación punto a punto
- El costo de performance es mínimo (~1-2ms extra por TLS handshake)
- Siempre usa HTTPS para APIs con autenticación
Desglose de latencia de una petición HTTP
Cuando haces una petición HTTP, el tiempo total no es solo “el servidor piensa”. Hay múltiples etapas, cada una con su costo:
t₀ t₁ t₂ t₃ t₄ t₅ t₆
│ │ │ │ │ │ │
├───────────┼──────────┼──────────┼──────────┼──────────────┼───────────┤
│ │ │ │ │ │ │
│ DNS │ TCP │ TLS │ Enviar │ Servidor │ Recibir │
│ lookup │ hand- │ hand- │ request │ procesa │ response │
│ │ shake │ shake │ │ │ │
│ ~5ms │ ~10ms │ ~15ms │ ~2ms │ ~1500ms │ ~5ms │
│ │ │ │ │ │ │
│ ¿quién es │ SYN → │ certifi- │ bytes │ inferencia │ bytes │
│ api. │ SYN-ACK →│ cados, │ del │ del LLM, │ de la │
│ anthropic │ ACK │ claves │ request │ base de │ respuesta │
│ .com? │ │ simétr. │ viajan │ datos, etc. │ viajan │
└───────────┴──────────┴──────────┴──────────┴──────────────┴───────────┘
│
│ ← ~97% del tiempo
│ para APIs de LLM
$$T_{total} = T_{DNS} + T_{TCP} + T_{TLS} + T_{req} + T_{server} + T_{resp}$$
Para una llamada típica a un LLM:
$$T_{total} \approx 5 + 10 + 15 + 2 + 1500 + 5 = 1537\text{ms}$$
Observa que $T_{server}$ domina con ~97% del tiempo total. Esto tiene dos implicaciones:
- Optimizar DNS, TCP o TLS (connection pooling, HTTP keep-alive) ayuda marginalmente
- La verdadera optimización es no esperar a que $T_{server}$ termine completamente antes de empezar a procesar — esto motiva el streaming (Sección 04)
Connection pooling y keep-alive
En la práctica, no pagas DNS + TCP + TLS en cada petición. HTTP/1.1 mantiene conexiones abiertas (keep-alive) y las librerías como requests.Session() y httpx.Client() reutilizan conexiones:
Primera petición:
DNS + TCP + TLS + request + server + response = ~1537ms
Siguientes peticiones (misma conexión):
request + server + response = ~1507ms
Ahorro: ~30ms por petición (2%)
# SIN pooling (nueva conexión cada vez)
for prompt in prompts:
response = requests.post(url, headers=h, json=body) # DNS+TCP+TLS cada vez
# CON pooling (reutiliza conexión)
session = requests.Session()
session.headers.update(h)
for prompt in prompts:
response = session.post(url, json=body) # solo request+server+response
HTTP/1.1 vs HTTP/2 vs HTTP/3
La evolución de HTTP ha mejorado la eficiencia del transporte:
HTTP/1.1 (1997) HTTP/2 (2015) HTTP/3 (2022)
━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━ ━━━━━━━━━━━━━
┌──────┐ ┌──────┐ ┌──────┐
│req 1 │ ────▶ resp 1 │req 1 │─┐ │req 1 │─┐
└──────┘ │req 2 │─┤ multiplexado │req 2 │─┤ QUIC
┌──────┐ (espera) │req 3 │─┘ (una conexión) │req 3 │─┘ (UDP)
│req 2 │ ────▶ resp 2 └──────┘ └──────┘
└──────┘ │ │
┌──────┐ (espera) ▼ ▼
│req 3 │ ────▶ resp 3 ┌──────┐ ┌──────┐
└──────┘ │resp 1│ │resp 2│ ◀─ sin
│resp 3│ ◀─ en cualquier │resp 1│ head-of-
Una petición a la vez │resp 2│ orden │resp 3│ line
por conexión (head-of-line) └──────┘ └──────┘ blocking
Para APIs de LLM, HTTP/2 es el estándar actual. La mayoría de los proveedores (Anthropic, OpenAI) lo soportan. httpx usa HTTP/2 por defecto cuando está disponible; requests usa HTTP/1.1.
Resumen visual: el flujo completo
Tu programa Python
│
│ 1. Construir request
│ - método: POST
│ - URL: https://api.anthropic.com/v1/messages
│ - headers: api-key, content-type, version
│ - body: JSON con modelo, tokens, mensajes
│
▼
┌─────────────┐
│ requests │ ── serializar JSON, agregar headers
│ / httpx │
└──────┬──────┘
│
│ 2. Resolver DNS (api.anthropic.com → 104.18.x.x)
│ 3. TCP handshake (SYN, SYN-ACK, ACK)
│ 4. TLS handshake (certificados, claves)
│ 5. Enviar bytes del request
│
▼
┌─────────────┐
│ Servidor │ ── autenticar, validar, procesar
│ Anthropic │ ── inferencia en GPU (~1500ms)
└──────┬──────┘
│
│ 6. Enviar bytes de la response
│ - status: 200 OK
│ - headers: content-type, rate-limit info
│ - body: JSON con respuesta del LLM
│
▼
┌─────────────┐
│ requests │ ── deserializar JSON
│ / httpx │
└──────┬──────┘
│
│ 7. Tu código recibe un objeto Response
│ response.status_code → 200
│ response.json() → {"content": [...], "usage": {...}}
│
▼
Listo. Toda la magia de HTTP ocurrió entre los pasos 2-6,
invisible para tu código gracias a la librería.
Verifica en el notebook: Notebook 01 — Sección 2 contiene ejercicios prácticos donde construyes peticiones HTTP manualmente con
requests, inspeccionas headers, manejas errores por código de estado, y mides la latencia de cada etapa.
Usando requests en Python (o curl en terminal), realiza las siguientes peticiones y para cada una documenta: método, URL, headers enviados, status code recibido, y headers de respuesta relevantes.
GET https://httpbin.org/get— ¿Qué headers envíarequestspor defecto?POST https://httpbin.org/postcon body{"curso": "fdd", "seccion": 18}— ¿Cómo se ve el body en la respuesta de httpbin?GET https://httpbin.org/status/429— ¿Qué status code recibes? ¿Hay un headerretry-after?GET https://httpbin.org/delay/3— Mide el tiempo total contime.time(). ¿Cuánto tarda? ¿Dónde se va el tiempo según el desglose de latencia?
Bonus: repite la petición 4 pero usando requests.Session() dos veces seguidas. ¿La segunda es más rápida? ¿Por qué?