CodeGym /Cursos /Java Server /Leer la respuesta HTTP: estado, encabezados, cuerpo

Leer la respuesta HTTP: estado, encabezados, cuerpo

Java Server
Nivel 14, Lección 3
Disponible

1. De qué se compone una respuesta HTTP

Cuando empiezas a trabajar con APIs externas, es muy fácil caer en la trampa: «solo necesito el JSON, entonces tomo response.body() y ya está». Es como recibir una carta de papel y leer solo el mensaje principal, ignorando el asunto, el remitente, la dirección y las marcas en el sobre. A veces funciona. Pero a veces resulta que la carta en realidad es «Devolución. Destinatario no encontrado», solo que está escrita con una letra bonita.

En HTTP, la respuesta siempre tiene la misma estructura: hay un estado (un número que dice brevemente «éxito/error y de qué tipo»), hay encabezados (metadatos sobre el formato, la longitud, la hora, el tipo de contenido, etc.), y hay un cuerpo (payload — a menudo JSON, pero no siempre). En Java esto se refleja casi de manera idéntica en HttpResponse<String>: statusCode() da el estado, headers() da los encabezados, body() da el cuerpo en la forma que hayas solicitado con el body handler.

Para fijar la correspondencia «realidad HTTP ↔ métodos Java», es útil tener en mente una pequeña tabla:

Parte de la respuesta Qué significa en lenguaje humano Dónde está en HttpResponse<String>
Código de estado «Cómo evalúa el servidor la solicitud» (éxito, error del cliente, error del servidor) response.statusCode()
Encabezados «Información auxiliar: qué tipo de cuerpo, cómo leerlo, qué reglas aplicar» response.headers()
Cuerpo «Contenido: datos o mensaje de error» response.body()

Si solo recuerdas una idea de esta lección, que sea el orden: primero el estado, luego los encabezados, después el cuerpo. Es como una técnica de seguridad: primero miras el cartel de «tensión», luego metes mano al cableado.

Se puede representar visualmente así:

flowchart TD
    A["Enviamos la HttpRequest"] --> B["Recibimos HttpResponse<String>"]
    B --> C["1) statusCode()"]
    C --> D["2) headers()"]
    D --> E["3) body()"]
    E --> F["Mostramos al usuario / diagnosticamos el contrato"]

2. status code: resumen rápido de la solicitud

El código de estado es la forma más rápida de entender qué ha ocurrido. Y no es una «formalidad» ni algo «para que quede bonito»: el estado es un canal de significado independiente. El servidor puede devolver un JSON que tiene «pinta de ser normal», pero si el estado no es el adecuado, ya es otro escenario. En las integraciones, a menudo se rompe no «lo que son los datos», sino «el significado»: crees que has recibido la ficha de un libro, pero en realidad has recibido un error de «no encontrado» o «demasiadas solicitudes».

En el contexto de aprendizaje, nos basta con un modelo práctico sencillo: 2xx es éxito, todo lo demás es motivo para detenerse y al menos mirar qué está mal. Sí, en el mundo existen 3xx, redirecciones y demás, pero hoy nos centramos en la disciplina básica de leer la respuesta.

Para que al estudiante le sea más fácil, se pueden usar las «clases» de estados (el primer dígito):

Clase Ejemplo Significado en pocas palabras
2xx 200 Éxito
4xx 404 Problema en la solicitud/recurso desde el punto de vista del cliente
5xx 500 Problema en el lado del servidor

Ahora, cómo expresar esto en código sin convertirlo todo en un enorme sistema de gestión de errores (no lo necesitamos ahora). Empecemos por lo más simple: imprimir el estado.

import java.net.http.HttpResponse;

// response lo obtuvimos después de client.send(...) — es nuestro «sobre» con la respuesta
HttpResponse<String> response = /* lo obtuvimos después de client.send(...) */;

// El estado es lo primero que hay que mirar: «éxito/error y de qué tipo»
int status = response.statusCode();
System.out.println("HTTP " + status); // HTTP 200 (u otro estado)

Parece trivial, pero realmente transforma «una cadena cualquiera» en «veo lo que el servidor ha dicho sobre la solicitud». A continuación, una comprobación mínima de «éxito o no»:

// Tomamos el estado por separado para no perder el sentido de la respuesta
int status = response.statusCode();

// Consideramos éxito 2xx, todo lo demás es motivo para frenar y diagnosticar
if (status >= 200 && status < 300) {
    System.out.println("Solicitud exitosa"); // Solicitud exitosa
} else {
    System.out.println("Solicitud fallida: HTTP " + status); // Solicitud fallida: HTTP 404
}

Y hay otro «truco» que les gusta a los desarrolladores backend por su concisión: dividir entre 100. Esto ayuda a leer la clase de estado sin recordar rangos.

int status = response.statusCode();

// Dividimos entre 100 para obtener la clase: 2xx/4xx/5xx, etc.
int statusClass = status / 100;

if (statusClass == 2) {
    System.out.println("2xx éxito"); // 2xx éxito
} else if (statusClass == 4) {
    System.out.println("4xx problema del cliente"); // 4xx problema del cliente
} else if (statusClass == 5) {
    System.out.println("5xx problema del servidor"); // 5xx problema del servidor
}

Importante: esto no es un tratamiento de errores «ideal». Es simplemente una forma disciplinada de no pasar por alto la realidad. En el proyecto ReadLater Starter, esta disciplina es especialmente útil porque accedemos a un catálogo externo de libros: si devuelve 404 o 503, debes verlo de inmediato, no «analizar el JSON» y preguntarte por qué los campos no coinciden.

3. headers: metadatos de la respuesta

Los encabezados a menudo se perciben como algo secundario: «bueno, unas cadenas…». Pero para un cliente HTTP, los encabezados son pistas: qué contiene el cuerpo, cómo está codificado, qué formato esperar y, a veces, incluso por qué el servidor tomó tal o cual decisión. Y en un proyecto de aprendizaje, son útiles al menos como herramienta para verificar el contrato: realmente nos enviaron JSON, y no una página HTML con «Acceso denegado» disfrazada de texto bonito.

En Java, los encabezados no están simplemente «en forma de Map», sino en un objeto HttpHeaders (método response.headers()). Tiene algo útil: puedes obtener todos los encabezados completos (map()), o un encabezado concreto (firstValue(...), allValues(...)). Y aquí hay un matiz importante: un mismo encabezado a veces tiene varios valores, por lo que a nivel de «mapa» ves Map<String, List<String>>.

Empecemos con solo ver todos los encabezados de una sola llamada:

// Una «radiografía» rápida: mostramos todos los encabezados de golpe (clave -> lista de valores)
System.out.println(response.headers().map());
// {content-type=[application/json], date=[...], ...}

Es una «radiografía» rápida: no estás obligado a entender cada encabezado, pero ya ves que, por ejemplo, content-type existe y parece JSON.

Si quieres mostrar los encabezados de forma bonita y legible (y sin escribir 50 líneas), puedes hacerlo así:

// Recorremos todos los encabezados: un mismo nombre puede tener varios valores
response.headers().map().forEach((name, values) -> {
    // Unimos la lista de valores en una sola cadena para una salida cómoda
    String joined = String.join(", ", values);
    System.out.println(name + ": " + joined);
});
// content-type: application/json
// date: Tue, 19 Mar 2026 12:00:00 GMT

Observa: values es una lista. Por eso String.join(", ", values) viene muy bien aquí.

Ahora, lo más práctico para nosotros: el encabezado Content-Type. Responde a la pregunta: «¿Y qué hay en el cuerpo?». Si Content-Type = application/json, la probabilidad de que realmente haya JSON en el cuerpo aumenta drásticamente. Si es text/html, lo más probable es que te hayan devuelto una página HTML (quizás un error o un placeholder).

Extraemos Content-Type de forma segura (puede faltar, especialmente en respuestas extrañas):

// firstValue(...) devuelve Optional: puede que no exista el encabezado
String contentType = response.headers()
        .firstValue("Content-Type")
        .orElse("<no Content-Type>");

System.out.println("Content-Type: " + contentType);
// Content-Type: application/json; charset=utf-8

Un detalle pequeño pero importante: a menudo Content-Type contiene no solo el tipo, sino también parámetros adicionales, como charset. Por lo tanto, es mejor comprobar «si es JSON» usando contains("application/json") en lugar de una igualdad estricta.

String contentType = response.headers()
        .firstValue("Content-Type")
        .orElse("")]

// Comprobación «parece JSON»: teniendo en cuenta que pueden haber parámetros como charset
boolean jsonLike = contentType.contains("application/json");
System.out.println("Es JSON: " + jsonLike); // Es JSON: true

Otro detalle con el que a veces tropiezan los principiantes: los nombres de los encabezados en HTTP no distinguen entre mayúsculas y minúsculas, pero cuando imprimes headers().map(), las claves pueden aparecer en un formato determinado (a menudo en minúsculas). Así que no te sorprendas si esperabas Content-Type y ves content-type. Para leerlo mediante firstValue("Content-Type") puedes escribir el nombre habitual: Java lo manejará correctamente.

4. Raw body: cadena y preview

El cuerpo de la respuesta es por lo que hicimos la solicitud en primer lugar. Pero «cuerpo» es un concepto engañoso: puede ser corto, puede ser enorme, puede ser JSON, o puede resultar ser cualquier cosa (por ejemplo, texto de error, HTML, texto plano). Y lo que es más importante: el cuerpo siempre debe leerse en el contexto del estado y los encabezados, de lo contrario corres el riesgo de interpretar «algo que no es».

En nuestro modelo actual de HttpResponse<String>, el cuerpo es simplemente una cadena:

// En este modelo, el body ya es una cadena (elegimos el body handler correspondiente)
String body = response.body();

// Es útil conocer el tamaño antes de imprimir «todo»
System.out.println("Longitud del body: " + body.length()); // Longitud del body: 8421

Si ahora haces System.out.println(body), en el mejor de los casos te ahogarás en JSON, y en el peor, obtendrás una salida tan grande que dejarás de ver lo importante (por ejemplo, el estado y los encabezados). Por eso, en la práctica a menudo se hace un preview: se imprimen los primeros N caracteres para comprobar visualmente «si se parece a lo que esperábamos».

Aquí tienes la forma más segura de hacer un preview sin obtener un StringIndexOutOfBoundsException:

String body = response.body();

int max = 200;

// Recortamos de forma segura: si el cuerpo es más corto que max, tomamos toda la longitud
String preview = body.substring(0, Math.min(body.length(), max));

System.out.println("Preview del body: " + preview);
// Preview del body: {"items":[{"title":"Clean Code",...

Si quieres algo más cuidado (para que se vea que el texto está recortado), puedes hacer una pequeña utilidad:

private static String preview(String text, int maxLen) {
    // Protección contra null: a veces el body handler/el código circundante puede devolver null
    if (text == null) return "<null>";

    // Si el texto es corto, lo devolvemos tal cual
    if (text.length() <= maxLen) return text;

    // Si es largo, recortamos y añadimos «...» para que se vea el hecho del recorte
    return text.substring(0, maxLen) + "...";
}

Y se usa así:

String body = response.body();

// Imprimimos solo el principio para no «llenar» la consola
System.out.println(preview(body, 200));
// {"items":[{"title":"Clean Code",...}

Observa la combinación muy útil: primero miramos el Content-Type, luego decidimos qué hacer con el body. Por ejemplo, si Content-Type no parece JSON, no intentes «hacerte el listo», sino muestra honestamente un preview como texto para diagnóstico.

String contentType = response.headers().firstValue("Content-Type").orElse("");
String body = response.body();

// Si el tipo no se parece a JSON, mostramos un mensaje de diagnóstico y un trozo del cuerpo
if (!contentType.contains("application/json")) {
    System.out.println("Tipo de body inesperado: " + contentType);
    System.out.println("Preview del body: " + preview(body, 200));
}

Esta comprobación no hace que tu cliente sea «inteligente». Lo hace no ingenuo. Y créeme, a nivel de integraciones eso ya es la mitad de la batalla.

Y otra idea que encaja bien con el proyecto ReadLater Starter: el raw body es útil como «primera comprobación del contrato». Es decir, no mapeamos JSON, no construimos DTO, no formateamos bonito — simplemente nos aseguramos de que la respuesta ha llegado, el estado es adecuado, el formato se parece al esperado y el cuerpo parece JSON. Es como primero comprobar que la bicicleta tiene ruedas, y solo luego discutir qué pegatinas deberían tener.

5. Diagnóstico: resumen de la respuesta

Cuando haces tus primeras llamadas externas, es muy útil tener un pequeño fragmento de código que, a partir de HttpResponse<String>, imprima «lo más importante». Esto ayuda a mantener la mecánica visible, pero sin convertir cada lugar de la aplicación en un copia-pega de statusCode()/headers()/substring().

Empecemos con un mini-método «imprimir informe breve» — es deliberadamente simple. Su idea es darte una vista rápida de la respuesta, sin convertir este paso en una arquitectura separada.

import java.net.http.HttpResponse;

public static void printSummary(HttpResponse<String> response) {
    // 1) Estado: resumen general «éxito/error»
    int status = response.statusCode();

    // 2) Encabezado clave: qué tipo de datos hay en el cuerpo
    String contentType = response.headers().firstValue("Content-Type").orElse("<none>");

    // 3) Cuerpo: imprimimos solo el principio para no ahogarnos en la salida
    String bodyPreview = preview(response.body(), 200);

    System.out.println("HTTP " + status + ", Content-Type=" + contentType);
    System.out.println("Body: " + bodyPreview);
}

Aquí está exactamente lo que necesitamos hoy: estado, un encabezado clave y los «primeros 200 caracteres». Y nada de «frameworks universales» que normalmente empeoran las cosas en el primer paso: ya no entiendes qué ha llegado realmente y por qué.

Si quieres, puedes añadir una capa adicional de significado: imprimir «OK» o «FAIL» según el estado. Esto ayuda cuando ejecutas la aplicación rápidamente y no quieres buscar visualmente HTTP 200 entre otras cosas.

int status = response.statusCode();

// Etiqueta simple: éxito solo para 2xx
boolean ok = status >= 200 && status < 300;

System.out.println((ok ? "[OK] " : "[FAIL] ") + "HTTP " + status);
// [OK] HTTP 200

Desde el punto de vista de ReadLater Starter, se vería así: enviaste una solicitud de búsqueda, obtuviste HttpResponse<String>, llamaste a printSummary(response) y viste en la consola que realmente es un 200, que Content-Type parece JSON y que el body empieza como el JSON esperado. Para el día de hoy, eso ya es un resultado excelente: no «crees», «ves».

6. Errores típicos al leer la respuesta

Error nº1: mirar solo body() e ignorar statusCode().
Es muy fácil caer en el modo «necesito JSON — dame JSON», e imprimir directamente response.body(). El problema es que el cuerpo sin estado pierde su significado. Incluso si el servidor devuelve JSON, puede ser un JSON de error, un JSON ficticio o simplemente el resultado de otro escenario. El hábito de «estado primero» casi siempre ahorra horas de depuración.

Error nº2: imprimir en la consola todo el cuerpo de la respuesta.
En los primeros pasos parece lógico hacer System.out.println(response.body()) para «ver qué ha llegado». Pero las respuestas reales suelen ser grandes y tú mismo te ahogarás en los datos. Peor aún: entre el JSON gigante dejarás de ver cosas importantes: el estado, Content-Type, tus propios mensajes de diagnóstico. Un preview de 100-200 caracteres da exactamente lo necesario para una comprobación inicial y no convierte la consola en un vertedero.

Error nº3: hacer substring(0, 200) sin comprobar la longitud de la cadena.
Es un clásico: en el camino feliz todo va bien, pero luego te encuentras con una respuesta corta (o un cuerpo vacío) y de repente te sale un StringIndexOutOfBoundsException. Al final, en lugar de diagnosticar un problema HTTP, diagnosticas tu propio error de cadena. Math.min(body.length(), 200) es un pequeño truco que hace que tu código sea notablemente más tranquilo.

Error nº4: pensar que si el cuerpo «parece JSON», entonces todo ha ido bien.
A veces el servidor devuelve un error en formato JSON, y eso es normal. Pero si no miras el estado, puedes confundir un error con una respuesta exitosa y empezar a trabajar «alegremente» con datos incorrectos. En un proyecto de aprendizaje, esto se manifestará rápido: esperas una lista de libros y recibes un JSON con un error, y luego te preguntas por qué «no existe el campo items».

Error nº5: ignorar por completo los encabezados y luego preguntarse «por qué no se analiza».
Incluso si hoy no hacemos mapeo a DTO, el encabezado Content-Type ya te da una pista enorme: el servidor realmente devolvió JSON o devolvió otra cosa. Cuando se ignoran los encabezados, la depuración se convierte en chamanismo: «bueno, parece una cadena… seguro que es JSON… pero no funciona…». Un simple Content-Type en la salida a menudo pone todo en su lugar.

1
Tarea
Java Server, nivel 14, lección 3
Bloqueada
Resumen de respuesta HTTP exitosa
Resumen de respuesta HTTP exitosa
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION