Módulo 2: El script de k6 y los usuarios virtuales
3. `http.get` y `http.post` con cuerpo y headers
Descripción
Un VU que no hace peticiones no prueba nada. El guion default cobra sentido cuando dentro de él el usuario virtual habla con la API: pide la lista de salas, cotiza una reserva, confirma una compra. En k6, esa conversación ocurre a través del módulo k6/http —http.get para leer, http.post para enviar—. En esta lección llenamos la vuelta del guion con peticiones reales a la API de Reservo, y aprendemos las dos piezas que todo POST a una API JSON necesita: el cuerpo (el JSON que mandas) y los headers (el Content-Type que le dice al servidor cómo leer ese cuerpo).
Es la misma mecánica de cualquier cliente HTTP, pero con la forma exacta que k6 espera. Un GET es simple: una URL y listo. Un POST a una API JSON tiene tres partes que hay que ensamblar bien —la URL, el cuerpo serializado con JSON.stringify, y los headers en un objeto params—; equivocarse en el orden o en el header es el error más común de quien empieza, y lo veremos con detalle. Y una vez que la petición vuelve, hay que leer la respuesta: su status (¿200?) y su cuerpo JSON (res.json()), que en la lección 4 alimentará el check().
Conexión con el módulo: la lección 2 te dio el bucle vacío; esta lo llena de contenido. Ahora el VU no solo "da vueltas": en cada vuelta pide /rooms o cotiza en /quote con su cuerpo {room, tier, hours} y recibe {price_cents}. El código de k6 se muestra como contenido; el equivalente en Python con urllib.request se ejecuta de verdad contra la API canónica, y verás los números ancla —7500 y 6000— salir del servidor real. Todo lo rotulado como salida de Python fue medido en este entorno con Python 3.14.0. La respuesta que aquí solo leemos, en la lección 4 la verificaremos con check().
Pedir por teléfono a la cocina
Piénsalo así. Llamas a un restaurante que también hace entregas. Hay dos tipos de llamada. La primera es una consulta: "¿qué salas... perdón, qué platillos tienen hoy?" —solo preguntas, no mandas nada, y te leen la carta—. La segunda es un pedido: "quiero el platillo tal, para tantas personas, a tal hora" —aquí sí mandas información, y tienes que darla en un formato que la cocina entienda; si hablas en un idioma que no manejan, no pueden tomar la orden aunque grites—.
http.get es la consulta: pides una URL y te devuelven lo que hay ahí (la lista de salas de /rooms). No mandas datos, solo preguntas. http.post es el pedido: mandas un cuerpo (los datos de tu reserva) y, crucialmente, un header que dice en qué idioma va ese cuerpo —Content-Type: application/json, "esto es JSON, léelo como JSON"—. Si olvidas el header, es como pedir en un idioma que la cocina no reconoce: el servidor recibe los bytes pero no sabe que son JSON, y tu pedido puede fallar o malinterpretarse. El cuerpo es qué pides; el header es cómo está escrito.
http.get(url)es una consulta: pides y lees, sin mandar datos.http.post(url, body, params)es un pedido: mandas un cuerpo (el JSON conJSON.stringify) y unos headers (elContent-Type: application/jsonque le dice al servidor que el cuerpo es JSON). Olvidar el header es como pedir en un idioma que la cocina no entiende.
http.get: leer la lista de salas (contenido)
El caso más simple: pedir GET /rooms para ver qué salas hay y a qué tarifa. En k6:
// rooms.js — leer la lista de salas de Reservo.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
const BASE_URL = 'http://localhost:8000';
export default function () {
// GET: una consulta. Solo la URL, sin cuerpo.
const res = http.get(`${BASE_URL}/rooms`);
// Leer la respuesta:
// res.status -> el codigo HTTP (200, 404, ...)
// res.json() -> el cuerpo parseado como objeto JavaScript
console.log(res.status); // 200
console.log(res.json('rooms')); // la lista de salas
}
Tres cosas de http.get:
- Solo necesita la URL. Un
GETno manda cuerpo; por esohttp.getrecibe únicamente la dirección. Es la forma de leer un recurso. reses la respuesta.http.getdevuelve un objeto respuesta con todo lo que el servidor contestó:res.status(el código HTTP),res.body(el cuerpo crudo), yres.json()(el cuerpo ya parseado como objeto, si es JSON).res.json('rooms')lee el camporoomsdel JSON de respuesta. Puedes escribirres.json()para el objeto completo y luego.rooms, o pasar la ruta directamente como aquí. En la lección 4 usaremosres.json('price_cents')de la misma forma.
http.post: cotizar con cuerpo y headers (contenido)
Ahora el caso que importa: cotizar en POST /quote. Aquí sí mandamos datos —{room, tier, hours}— y hay que ensamblar las tres partes con cuidado:
// quote.js — cotizar una sala en Reservo.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
const BASE_URL = 'http://localhost:8000';
export default function () {
// 1) El cuerpo: el objeto JS convertido a texto JSON con JSON.stringify.
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
// 2) Los headers: le decimos al servidor que el cuerpo es JSON.
const params = { headers: { 'Content-Type': 'application/json' } };
// 3) La peticion: url, cuerpo, params (en ese orden).
const res = http.post(`${BASE_URL}/quote`, payload, params);
console.log(res.status); // 200
console.log(res.json('price_cents')); // 7500
}
Desármalo, porque estas tres partes son el molde de todo POST a una API JSON:
- El cuerpo (
payload). La API espera JSON, perohttp.postmanda texto. Por eso envuelves tu objeto enJSON.stringify(...): convierte{ room: 'Focus', tier: 'basic', hours: 3 }en la cadena'{"room":"Focus","tier":"basic","hours":3}'. Si le pasaras el objeto sinstringify, k6 lo enviaría como un formulario (application/x-www-form-urlencoded), no como JSON —un error clásico que veremos en "Errores comunes"—. - Los headers (
params). El tercer argumento dehttp.postes un objeto de opciones; dentro,headerslleva elContent-Type: application/json. Este header es la etiqueta que le dice al servidor "el cuerpo que sigue es JSON, parséalo como JSON". Sin él, muchas APIs rechazan o malinterpretan el cuerpo. - El orden:
http.post(url, body, params). La URL primero, el cuerpo segundo, los params tercero. Invertir cuerpo y params es un error silencioso: el servidor no recibe tu JSON donde lo espera.
La respuesta se lee igual que con GET: res.status (esperamos 200) y res.json('price_cents') (esperamos 7500). Ese 7500 es el número ancla —Focus a 2500 centavos por hora × 3 horas— que ya conoces del módulo 1.
Las mismas peticiones, ejecutadas en Python
Ahora la cara que sí se ejecuta. En Python, la biblioteca estándar urllib.request hace exactamente lo mismo que k6/http, con las mismas tres partes en el POST. Este es el equivalente:
# Las mismas peticiones que en k6, con urllib de la biblioteca estandar.
import json, urllib.request
BASE_URL = "http://127.0.0.1:PORT" # el puerto real lo asigna el SO
def get_rooms():
# GET /rooms: una consulta, solo la URL.
with urllib.request.urlopen(f"{BASE_URL}/rooms") as res:
return res.status, json.loads(res.read())
def post_quote(room, tier, hours):
# POST /quote: cuerpo JSON + header Content-Type, como en k6.
payload = json.dumps({"room": room, "tier": tier, "hours": hours}).encode()
req = urllib.request.Request(
f"{BASE_URL}/quote",
data=payload, # el cuerpo (2)
headers={"Content-Type": "application/json"}, # los headers (3)
method="POST",
)
with urllib.request.urlopen(req) as res:
return res.status, json.loads(res.read())
El mapeo con k6 es directo: json.dumps(...) es el JSON.stringify(...); el argumento headers={...} es el params.headers; urllib.request.urlopen es la petición. Corramos las dos consultas y las dos cotizaciones ancla contra la API canónica.
Qué esperar. GET /rooms debe devolver las tres salas con su tarifa; POST /quote Focus/basic/3h debe dar 7500, y Focus/pro/3h debe dar 6000 (el mismo precio con el descuento pro entero del 20 %). Salida real en este entorno:
# GET /rooms
status = 200
{'rooms': [{'room': 'Focus', 'rate_cents': 2500}, {'room': 'Studio', 'rate_cents': 4000}, {'room': 'Boardroom', 'rate_cents': 8000}]}
# POST /quote {"room":"Focus","tier":"basic","hours":3}
status = 200
{'price_cents': 7500}
# POST /quote {"room":"Focus","tier":"pro","hours":3}
status = 200
{'price_cents': 6000}
Léela con calma:
GET /rooms→ 200 + la lista — el servidor devolvió las tres salas (Focus2500,Studio4000,Boardroom8000 centavos por hora). Es la consulta: preguntas y te leen la carta.POST /quoteFocus/basic/3h → 7500 — el pedido con cuerpo funcionó. 2500 centavos/hora × 3 horas = 7500 centavos ($75.00). El headerContent-Type: application/jsonfue lo que permitió que el servidor leyera el cuerpo como JSON.POST /quoteFocus/pro/3h → 6000 — el mismo cuerpo pero contier: 'pro'. El servidor aplica el descuento pro entero: 7500 × 80 // 100 = 6000 centavos ($60.00). Es el segundo número ancla de la guía.
Estos son los mismos valores que el script de k6 recibiría —res.json('price_cents') daría 7500 y 6000— porque golpean la misma API canónica. La única diferencia es quién manda la petición: k6 (contenido) o urllib (ejecutado). El servidor no nota la diferencia.
Leer la respuesta: status y el cuerpo JSON
Mandar la petición es la mitad; la otra mitad es leer lo que vuelve, porque de eso dependerá el check() de la lección 4. Dos campos importan:
res.status— el código HTTP.200significa "OK, aquí está tu respuesta";400sería "tu petición está mal" (por ejemplo, horas inválidas);404, "ese endpoint no existe". Verificarstatus === 200es la primera comprobación de toda prueba de carga: ¿la API siquiera respondió bien?res.json('campo')— el cuerpo parseado. La API de Reservo responde JSON ({"price_cents": 7500}), yres.json('price_cents')extrae ese7500como número. Ojo con el tipo: la API manda centavos como entero (7500, no75.0ni"7500"), así que la comparación en elcheckserá con un entero (=== 7500). Ese detalle —dinero en centavosint— evita los errores de redondeo de los flotantes y es una convención de toda la guía.
Con estos dos campos en la mano, el VU ya no solo habla con la API: puede entender lo que le contesta. En la lección 4 convertimos ese entendimiento en una verificación formal con check().
Errores comunes
Olvidar JSON.stringify en el cuerpo del POST. Qué pasa: alguien escribe http.post(url, { room: 'Focus', ... }, params) pasando el objeto directamente. Por qué pasa: parece natural mandar el objeto tal cual. Cómo detectarlo: k6 serializa el objeto como un formulario (room=Focus&tier=basic&hours=3, con Content-Type de formulario), la API espera JSON y responde 400 o interpreta mal el cuerpo. Cómo corregirlo: envuelve siempre el objeto en JSON.stringify(...) para un cuerpo JSON, y acompáñalo del header Content-Type: application/json. En Python, el equivalente es json.dumps(...).encode().
Olvidar el header Content-Type. Qué pasa: mandas el JSON con JSON.stringify pero sin el objeto params de headers. Por qué pasa: el cuerpo ya es JSON, así que uno asume que el servidor lo notará. Cómo detectarlo: algunas APIs lo toleran, pero muchas responden 400 o 415 ("Unsupported Media Type") porque, sin el header, no saben que deben parsear el cuerpo como JSON. Cómo corregirlo: incluye siempre const params = { headers: { 'Content-Type': 'application/json' } } y pásalo como tercer argumento. El header es la etiqueta del idioma; no lo omitas.
Invertir el orden body/params en http.post. Qué pasa: alguien escribe http.post(url, params, payload) —los headers donde va el cuerpo—. Por qué pasa: son dos objetos y es fácil confundir cuál va primero. Cómo detectarlo: la API no recibe tu JSON como cuerpo (recibe el objeto de headers), y responde con un error de validación aunque tu payload esté perfecto. Cómo corregirlo: memoriza la firma http.post(url, body, params) —URL, cuerpo, params, en ese orden—. El cuerpo es el segundo argumento, siempre.
Ejercicios
Ejercicio 1 — Arma el POST. Escribe (en k6, como contenido) la petición para cotizar la sala Studio, tier pro, 2 horas. Incluye el cuerpo con JSON.stringify, los headers y la llamada http.post en el orden correcto.
Ver solución
const payload = JSON.stringify({ room: 'Studio', tier: 'pro', hours: 2 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(`${BASE_URL}/quote`, payload, params);
- El cuerpo va con
JSON.stringifypara que salga como texto JSON. - El header
Content-Type: application/jsonacompaña al cuerpo. - El orden es
http.post(url, payload, params): URL, cuerpo, params.
El precio esperado sería Studio (4000 centavos/hora) × 2 horas = 8000, con descuento pro: 8000 × 80 // 100 = 6400 centavos.
Ejercicio 2 — ¿get o post? Para cada acción, di si usarías http.get o http.post, y por qué: (a) obtener la lista de salas disponibles; (b) cotizar una reserva concreta; (c) confirmar una reserva enviando los datos del cliente.
Ver solución
- (a)
http.get(BASE_URL + '/rooms')— es una consulta: solo lees un recurso, no mandas datos. - (b)
http.post(BASE_URL + '/quote', payload, params)— es un pedido: mandas{room, tier, hours}en el cuerpo para que la API calcule el precio. - (c)
http.post(BASE_URL + '/book', payload, params)— otro pedido: mandas los datos de la reserva y el servidor la crea y confirma. Enviar datos que cambian o crean algo va porPOST.
Ejercicio 3 — Diagnostica el 400. Un compañero cotiza en /quote y siempre recibe status = 400 aunque su objeto { room: 'Focus', tier: 'basic', hours: 3 } se ve correcto. Su llamada es: http.post(url, { room: 'Focus', tier: 'basic', hours: 3 }). ¿Qué dos cosas están mal y cómo lo arreglas?
Ver solución
Faltan dos de las tres partes de un POST JSON:
- No serializó el cuerpo con
JSON.stringify. Pasó el objeto directo, así que k6 lo manda como formulario, no como JSON. La API espera JSON y no encuentra los campos donde los busca. - No incluyó el header
Content-Type: application/json. Sin él, el servidor no sabe que debe parsear el cuerpo como JSON.
Arreglo:
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(url, payload, params);
Con el cuerpo serializado y el header presente, la API recibe el JSON como lo espera y responde 200 con {"price_cents": 7500}.
Resumen y siguiente paso
En esta lección el VU aprendió a hablar con la API. Viste http.get(url) para leer un recurso (la lista de salas de /rooms) y http.post(url, body, params) para enviar datos —las tres partes que todo POST JSON necesita: el cuerpo con JSON.stringify, los headers con Content-Type: application/json, y el orden correcto—. Y aprendiste a leer la respuesta: res.status (¿200?) y res.json('campo') (el cuerpo JSON, con el dinero en centavos int). Lo ejecutaste de verdad con urllib.request de Python contra la API canónica y viste salir los números ancla: /rooms con las tres salas, /quote Focus/basic/3h = 7500 y Focus/pro/3h = 6000. Las mismas peticiones, la misma API; solo cambia quién las manda.
Antes de avanzar deberías poder: escribir un http.get y un http.post con cuerpo JSON y headers en el orden correcto; explicar por qué JSON.stringify y el Content-Type son obligatorios en un POST JSON; y leer res.status y res.json('campo') de la respuesta.
Hasta aquí solo hemos leído la respuesta. La lección 4 la verifica: con check() comprobaremos que la API no solo respondió, sino que respondió bien —status 200 y price_cents correcto— incluso bajo carga, y veremos qué pasa (con un bug real) cuando el precio no cuadra.
Recursos
- Peticiones HTTP en k6 —
http.get/http.post— la referencia de cómo mandarGETyPOSTcon cuerpo y headers, con la firmahttp.post(url, body, params). La fuente exacta de esta lección. - El módulo
k6/http— objeto Response — todo lo que trae la respuesta:status,body,json(). Cómo leer lo que la API contesta. urllib.request— Documentación de Python — el cliente HTTP de la biblioteca estándar con el que ejecutamos las mismas peticiones.Request(url, data, headers, method)es el molde delPOST.json— Documentación de Python —json.dumps(elJSON.stringifyde Python) yjson.loadspara serializar y parsear el cuerpo. La pieza que arma y lee el JSON.