Anatomía de una petición
Método, URL, cabeceras y cuerpo, y lo que de verdad recibe el servidor.
En la lección anterior enviaste una petición escribiendo una URL en el navegador. El navegador rellenó todo lo demás por ti. Esta lección desmonta una petición para que sepas qué es cada pieza, porque cada test que escribas fijará algunas de estas piezas a propósito y comprobará cómo reacciona el servidor.
Una petición HTTP tiene cuatro partes:
- un método, que dice qué tipo de acción quieres
- una URL, que dice sobre qué cosa y en qué servidor quieres que se haga
- cabeceras, que llevan información adicional sobre la petición
- un cuerpo opcional, que lleva los datos que envías
Cómo es una petición en el cable
HTTP/1.1 es un protocolo de texto. Antes de cualquier cifrado, una petición son unas pocas líneas de texto plano. Esta es la petición que curl envía para https://api.testingapis.com/api/echo?color=red. Puedes verla tú mismo ejecutando curl con -v (modo detallado): las líneas que empiezan por > son lo que salió.
GET /api/echo?color=red HTTP/1.1
Host: api.testingapis.com
User-Agent: curl/8.18.0
Accept: */*La primera línea es la línea de petición: el método, la ruta con su query string y la versión del protocolo. Cada línea siguiente es una cabecera, un nombre y un valor separados por dos puntos. Una línea vacía cierra las cabeceras. Si hubiera cuerpo, iría después de esa línea vacía. Las versiones más nuevas de HTTP (HTTP/2 y HTTP/3) envían la misma información en formato binario, pero las partes son idénticas, y todas las herramientas te las siguen mostrando en esta forma de texto.
Ver lo que recibió el servidor
La forma más fácil de aprender cómo son las peticiones es enviar una a un servidor que te diga qué recibió. /api/echo hace justo eso. Acepta cualquier método y responde con una descripción de tu petición. Envía esta:
/api/echo?color=red&size=10Esta es la respuesta para curl, recortada a los campos interesantes. Si la envías desde la página, las cabeceras serán las de tu navegador.
{
"method": "GET",
"url": "http://api.testingapis.com/api/echo?color=red&size=10",
"path": "/api/echo",
"queryString": "?color=red&size=10",
"query": { "color": "red", "size": "10" },
"headers": {
"Accept": "*/*",
"Host": "api.testingapis.com",
"User-Agent": "curl/8.18.0",
"Accept-Encoding": "gzip, br",
"Cf-Ipcountry": "IN",
"X-Forwarded-For": "203.0.113.7, 172.71.198.178, 10.30.107.46",
"X-Forwarded-Proto": "https"
},
"contentType": null,
"contentLength": 0,
"body": null
}Hay dos cosas ahí que merecen una segunda mirada. El servidor vio la URL como http://, no https://. Y recibió cabeceras que tú nunca enviaste, como Cf-Ipcountry y X-Forwarded-For. Tu petición pasó por una CDN y un balanceador de carga antes de llegar a la aplicación, y cada uno reescribió o añadió algo. La primera dirección de X-Forwarded-For es la tuya (la de arriba es un ejemplo). Esto es normal en sistemas en producción, y es la razón por la que un endpoint de eco es útil: muestra lo que llegó, no lo que crees que enviaste.
El método
El método es el verbo. GET pide datos. POST envía datos para crear algo. PUT, PATCH y DELETE cambian o eliminan cosas. La barra de direcciones del navegador solo puede enviar GET, y por eso necesitas otras herramientas para el resto. Los métodos tienen su propia lección, Métodos HTTP, así que por ahora basta con saber que la misma URL puede hacer cosas distintas según el método.
La URL
Descompón esta URL en sus partes:
https://api.testingapis.com/api/Products?category=books&limit=5| Parte | Valor | Significado |
|---|---|---|
| Esquema | https | Cómo conectarse. https es HTTP dentro de una conexión cifrada. |
| Host | api.testingapis.com | Qué servidor. También se envía como la cabecera Host. |
| Ruta | /api/Products | Qué recurso de ese servidor. |
| Query string | category=books&limit=5 | Parámetros extra, después del ?, separados por &. |
Las URLs también pueden llevar un puerto (:8080 después del host) y un fragmento (#section al final). Verás puertos a menudo en entornos de pruebas y locales. Los fragmentos nunca llegan al servidor: se los queda el navegador.
Parámetros de ruta y parámetros de consulta
Los valores pueden viajar en la ruta o en la query string. En /api/Products/1, el 1 es un parámetro de ruta. Identifica un producto concreto, y la documentación escribe la ruta como /api/Products/{id}. En /api/Products?limit=5, limit es un parámetro de consulta. Los parámetros de consulta suelen filtrar, ordenar o paginar resultados, y suelen ser opcionales. Esa división es una convención, no una regla, así que mira la documentación de cada API.
Las rutas suelen distinguir mayúsculas y minúsculas, y los nombres de los parámetros de consulta normalmente también. Depende del framework del servidor, así que haz una prueba rápida con cualquier API nueva: envía /api/products en lugar de /api/Products y mira qué pasa.
Codificación por porcentaje
Algunos caracteres tienen una función dentro de una URL. ? empieza la query string, & separa parámetros, # empieza el fragmento, / separa segmentos de la ruta, y los espacios no están permitidos en absoluto. Para enviar uno de ellos como dato, lo sustituyes por % y el valor hexadecimal de su byte. Un espacio pasa a ser %20 y & pasa a ser %26. Esto se llama codificación por porcentaje, o codificación de URL.
Supón que quieres buscar "red shoes" y filtrar por una etiqueta cuyo valor es literalmente el texto a&b. Codificado, funciona:
/api/echo?q=red%20shoes&tag=a%26b{
"queryString": "?q=red%20shoes&tag=a%26b",
"query": { "q": "red shoes", "tag": "a&b" }
}Ahora envía la etiqueta sin codificar el ampersand:
/api/echo?tag=a&b{
"queryString": "?tag=a&b",
"query": { "tag": "a", "b": "" }
}Ahora el servidor ve dos parámetros: tag con el valor a, y un parámetro vacío llamado b. Ni error ni aviso. La petición "funcionó" y los datos están mal. Es una clase real de bug en clientes que construyen URLs pegando cadenas, y es la razón por la que los campos de búsqueda son buen sitio para escribir ampersands, signos más, barras y texto que no esté en inglés durante las pruebas. El codificador de URL de este sitio hace la conversión si la necesitas a mano.
Cabeceras
Las cabeceras son pares de nombre y valor que describen la petición. Los nombres no distinguen mayúsculas y minúsculas, así que content-type y Content-Type significan lo mismo. La mayoría de los clientes añaden varias cabeceras automáticamente. Las que más vas a fijar o comprobar son estas:
| Cabecera | Qué dice | Ejemplo |
|---|---|---|
Content-Type | El formato del cuerpo que envías. | application/json |
Accept | Los formatos que estás dispuesto a recibir. | application/json |
User-Agent | Qué cliente envió la petición. | curl/8.18.0 |
Authorization | Credenciales, como un token. | Bearer abc123 |
Host | Qué sitio del servidor. Los clientes la rellenan a partir de la URL. | api.testingapis.com |
Accept importa cuando una API puede responder en más de un formato. /api/formats/negotiateinicia sesión para verlo devuelve el mismo usuario en JSON por defecto, y en XML si lo pides:
/api/formats/negotiateCompara el XML con el JSON que recibes sin la cabecera. Los dos llevan los mismos siete campos, escritos a la manera de cada formato: la lista roles se convierte en elementos role repetidos, y el manager nulo en un elemento vacío marcado con xsi:nil. Cuando una API ofrece varios formatos, prueba cada uno y no des por hecho que coinciden. Cuando se escribió esta lección, a la versión XML de este usuario le faltaban tres campos.
Authorization se trata a fondo en Autenticación y autorización. Una cosa que conviene saber ya: nunca pegues tokens reales en capturas de pantalla, informes de bugs ni colecciones compartidas. Son contraseñas.
El cuerpo
Las peticiones GET normalmente no tienen cuerpo. POST, PUT y PATCH suelen tenerlo, porque envían datos al servidor. El cuerpo puede estar en cualquier formato, y la cabecera Content-Type le dice al servidor cómo leerlo. En la mayoría de las APIs de hoy el cuerpo es JSON. Esto envía un pequeño objeto JSON al endpoint de eco con curl:
curl -X POST https://api.testingapis.com/api/echo \
-H "Content-Type: application/json" \
-d '{"title":"Dune","year":1965}'{
"method": "POST",
"path": "/api/echo",
"query": {},
"contentType": "application/json",
"contentLength": 28,
"body": "{\"title\":\"Dune\",\"year\":1965}",
"json": { "title": "Dune", "year": 1965 }
}El servidor leyó la cabecera, vio application/json y analizó los 28 bytes como un objeto. Puedes enviar la misma petición desde aquí:
/api/echo{"title":"Dune","year":1965}Cuando Content-Type y el cuerpo no coinciden
Ahora envía el mismo tipo de cuerpo con curl, pero sin la cabecera:
curl -X POST https://api.testingapis.com/api/echo \
-d '{"title":"Dune"}'{
"contentType": "application/x-www-form-urlencoded",
"contentLength": 16,
"body": "{\"title\":\"Dune\"}",
"json": null,
"form": { "{\"title\":\"Dune\"}": "" }
}La opción -d de curl usa por defecto application/x-www-form-urlencoded, el formato de los formularios HTML. El servidor creyó a la cabecera, intentó leer el cuerpo como un formulario y acabó con un único campo de formulario cuyo nombre es todo el texto JSON. No volvió ningún error. Muchos servidores reales se comportan igual, y otros devuelven 415 Unsupported Media Type. Un Content-Typeausente o equivocado es una de las razones más comunes por las que una petición "parece correcta" y aun así falla, así que es lo primero que hay que mirar cuando un servidor ignora tus datos.
Si la cabecera es correcta pero el JSON está roto, un buen servidor lo dice. Esta es parte de la respuesta del eco para el cuerpo {"title":"Dune",}, que tiene una coma final:
"json": null,
"jsonError": "The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. LineNumber: 0 | BytePositionInLine: 16."Ejercicio
Usa /api/echo para responder cada pregunta. Mira la respuesta cada vez en lugar de adivinar.
- Envía
/api/echo?name=Zoë&city=São Paulodesde la barra de direcciones del navegador. ¿Qué cambió el navegador en la URL antes de enviarla, y a qué lo decodificó el servidor? - Envía
/api/echo?id=1&id=2. ¿Cómo representa el servidor un parámetro que aparece dos veces? Los distintos frameworks no se ponen de acuerdo en esto, lo que lo convierte en una buena pregunta para cualquier API que pruebes. - Envía un POST con un cuerpo JSON y
Content-Type: text/plain. ¿Analizó el servidor el JSON de todos modos? - Prueba
/api/echo/user-agentdesde un navegador y desde curl. ¿Por qué le importaría a una API cuál usaste?