CodeGym /Cursos /Java Server /Respuestas no- 2xx y límite de errores

Respuestas no- 2xx y límite de errores

Java Server
Nivel 15, Lección 3
Disponible

1. HttpResponse — todavía no es una victoria

Cuando empiezas a escribir un cliente HTTP, el cerebro se acostumbra muy rápido a una lógica simple: “si client.send(...) devolvió HttpResponse, entonces todo está bien”. Y esto es cierto… aproximadamente en un 60%. La respuesta realmente llegó, significa que nos comunicamos, acordamos el protocolo, obtuvimos estado, cabeceras y cuerpo. Pero a partir de ahí comienza la parte adulta: el servidor podría haber dicho “te entendí, pero no estoy de acuerdo”.

Imagina que pides una pizza. Si el teléfono no contesta — es un problema de transporte: ni siquiera hablaste. Si te contestan y te dicen “hoy no trabajamos” — la conversación ocurrió, pero el resultado para ti sigue siendo “no hay pizza”. En HTTP es exactamente lo mismo: una excepción en send() — “el teléfono no contesta”, mientras que un HttpResponse con estado 404 o 500 — “respondieron, pero rechazaron”.

Con los timeouts, IOException e InterruptedException ya nos ocupamos: allí podría no haber respuesta en absoluto. Aquí es una rama diferente. HttpResponse ya llegó, pero usarlo como éxito solo es posible después de verificar el estado.

La idea clave aquí es simple: la presencia de HttpResponse significa que el nivel de transporte llegó al servicio remoto y obtuvo una respuesta, pero el éxito no lo determina el hecho de la respuesta, sino su statusCode().

Un pequeño ejemplo de cómo se ve el código “ingenuo” que se alegra demasiado pronto:

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// Importante: send() puede devolver con éxito un HttpResponse incluso en 404/500 — esto NO es una excepción.
HttpResponse<String> response = client.send(
        HttpRequest.newBuilder(URI.create("https://catalog.example/books/NOPE"))
                .header("Accept", "application/json") // Solicitamos JSON, pero esto no garantiza un estado exitoso
                .GET()
                .build(),
        HttpResponse.BodyHandlers.ofString() // Leemos el cuerpo como cadena, pero puede ser también un error
);

System.out.println(response.body()); // puede que no sea un libro, sino una descripción de error

Este código a menudo “funciona” justo hasta el primer escenario negativo. Y los escenarios negativos en el mundo de las redes llegan rápido, como notificaciones del banco después de comprar un café.

2. Tres resultados de la llamada: éxito y errores

Hasta que no desglosemos los resultados en categorías, el código del cliente será demasiado optimista (“todo es éxito”) o demasiado paranoico (“todo es error”). En la práctica tenemos tres situaciones fundamentalmente diferentes, y se distinguen por lo que exactamente tenemos “en las manos” después de la llamada.

A continuación, una tabla que ayuda a no confundir estos casos (y no capturar Exception para todo, como si fuera un parche universal).

Situación ¿Hay HttpResponse? Lo que sabemos Ejemplo típico Qué hacer en el código
Respuesta exitosa (2xx) Sí Estado de éxito, cabeceras, body (generalmente JSON) 200 OK en búsqueda,
200 OK
en detalles
Permitir leer el body como “datos”
Respuesta con error (non-2xx) Sí Estado de error, cabeceras, body (a menudo descripción del error) 404 Not Found,
429 Too Many Requests
,
500 Internal Server Error
No considerar el body como “datos”; procesar el estado y notificar el error
Error de transporte (excepción) No No hay respuesta, solo el hecho del problema (timeout, red, interrupción) HttpTimeoutException,
IOException
Procesar como fallo de comunicación; esto no es un “error de estado”, no hay estado en absoluto

Y un diagrama de flujo muy útil (y honesto). Es conveniente tenerlo en mente cuando escribes send():

flowchart TD
    A["client.send(request)"] -->|"HttpTimeoutException / IOException / InterruptedException"| B["Error de transporte: no hay respuesta"]
    A -->|"obtuvimos HttpResponse"| C{"statusCode 2xx?"}
    C -->|"sí"| D["Éxito: se puede leer el body como datos"]
    C -->|"no"| E["Error HTTP: estado != 2xx, body no = datos"]

Si recuerdas solo esto, ya será más fácil. Porque la principal confusión del principiante se ve así: “Obtuve JSON en el body, entonces todo está bien”. No. El JSON puede estar tanto en una respuesta exitosa como en un error. El estado es el interruptor principal de la rama de comportamiento.

3. Verificación del rango 200..299, no 200

Cuando escribes un cliente para un escenario específico (por ejemplo, GET búsqueda), a menudo llega un 200. Pero tan pronto como empiezas a pensar “como desarrollador de la capa de transporte”, debes ser un poco más amplio: los estados exitosos no son un solo número, sino un rango 2xx.

Si solo verificas status == 200, te crearás una “mini-lotería de bugs”. Por ejemplo, 201 Created también es éxito, solo que de otro tipo. 204 No Content es éxito sin body. Y sí, aunque hoy tu proveedor siempre devuelva 200, mañana puede cambiar (o irás a otro endpoint).

Función mínima pero muy útil:

private static boolean is2xx(int status) {
    // 2xx es el rango de éxito, no un solo valor 200
    return status >= 200 && status < 300;
}

Ahora puedes formular cualquier verificación de éxito “como un adulto”:

int status = response.statusCode(); // El estado decide: éxito o error

if (!is2xx(status)) {
    // No dejamos pasar el body como "datos" si el estado no es exitoso
    System.out.println("El catálogo devolvió un error: " + status);
    return;
}

System.out.println("Éxito: " + response.body()); // Aquí el body ya se puede interpretar como datos

Observa: aquí hicimos algo importante — no dejamos que el body pase adelante hasta asegurarnos de que el estado es exitoso. Es un hábito simple que ahorra muchos nervios cuando más tarde empieces a convertir el body en DTO (pero eso no será hoy).

Y un pequeño matiz más: cuando usas HttpResponse.BodyHandlers.ofString(), con 204 obtendrás simplemente una cadena vacía. Y es normal: “sin contenido” es un éxito legítimo. Para nuestro catálogo de libros, 204 difícilmente se encontrará, pero para el transporte general sí ocurre.

4. Body en errores: no son datos, sino una pista

Cuando el estado no es 2xx, surge la tentación de hacer una de dos acciones extremas. El primer extremo es ignorar el body por completo: “error y error”. El segundo extremo es leer alegremente el body como si fueran datos normales: “pero ahí hay texto/JSON, entonces está bien”.

La posición correcta está en algún punto intermedio: el body en un error a menudo es muy útil para el diagnóstico, pero no son datos del happy path. Para nuestro proyecto ReadLater Starter es suficiente por ahora una regla simple: en caso de non-2xx podemos conservar o mostrar el body como pista (dentro de límites razonables), pero no continuamos el procesamiento como si fuera el resultado de una búsqueda/detalles.

Para que “dentro de límites razonables” no sean solo palabras, agregaremos un pequeño helper que corta el body demasiado largo. Esto salva a la consola de convertirse en un rollo de 300 pantallas:

private static String shorten(String text, int maxLen) {
    // En los errores, el servidor puede enviar cualquier cosa y muy larga — limitamos la longitud
    if (text == null) return "<no body>";
    if (text.length() <= maxLen) return text;

    // Devolvemos solo el inicio para no convertir la salida en un texto interminable
    return text.substring(0, maxLen) + "...";
}

Ahora al ocurrir un error se puede mostrar algo ordenado:

int status = response.statusCode(); // Primero miramos el estado

if (!is2xx(status)) {
    // Imprimimos el body como pista (diagnóstico), no como "datos"
    System.out.println("Error del catálogo: " + status);
    System.out.println("Fragmento del body: " + shorten(response.body(), 200));
}

Además, es útil recordar que el servidor en un error puede enviar de repente no JSON, incluso si solicitaste JSON mediante Accept: application/json. A veces es una página HTML de un proxy, a veces texto plano, a veces “algo extraño”. Por eso en el diagnóstico a veces ayudan las cabeceras:

String contentType = response.headers()
        .firstValue("Content-Type") // La cabecera puede no existir — por eso la leemos mediante Optional
        .orElse("<unknown>");

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

Recuerdo de nuevo: ahora no estamos construyendo un “error-parser ideal”. Estamos construyendo un hábito básico: el estado decide la rama, y el body en caso de error es una pista, no un resultado.

5. 4xx y 5xx: reacción mínima

Cuando vemos non-2xx, eso no significa que debamos montar una enciclopedia de estados. Pero hay una división muy útil que ayuda tanto a ti como al usuario: errores del cliente y errores del servidor.

4xx generalmente significa: “la solicitud es mala, o el recurso no se encontró, o no se puede hacer así”. Un ejemplo frecuente para el catálogo: un externalId incorrecto, y el servidor honestamente dice 404 Not Found. Esto no es “el proveedor se rompió”, es “ese libro no existe”.

5xx significa: “el servicio no pudo procesar por sí mismo”. Esto ya está más cerca de “el proveedor está enfermo”, y a menudo poco depende de tu código.

En el proyecto es útil tener una traducción muy simple del estado a un mensaje para el usuario (sin análisis profundo del body por ahora):

private static String userMessageForStatus(int status) {
    // Aquí no hacemos un "manual HTTP", sino que damos a la persona una explicación comprensible
    if (status == 404) return "Libro no encontrado en el catálogo";
    if (status >= 500) return "Catálogo temporalmente no disponible (error del servidor)";

    // Para los demás estados — al menos mostramos el código para tener algo a qué aferrarse en el diagnóstico
    return "El catálogo devolvió un error: " + status;
}

¿Por qué es útil incluso “antes de una arquitectura bonita”? Porque nuestro ReadLater Starter ahora se ejecuta como una aplicación de consola con comandos. El usuario (es decir, tú mismo) debe entender lo que sucedió: “me equivoqué de ID” o “el proveedor está caído”.

Y un detalle cuidadoso sobre 3xx: Java HttpClient por defecto no sigue redirecciones. Si accidentalmente usas http://... en lugar de https://..., el proveedor puede responder con 301/302. Para una API JSON típica, esto a menudo es una señal de que baseUrl es incorrecto, y es mejor corregirlo que intentar “leer la redirección como datos”.

6. Regla de transporte: body solo en 2xx

Cuando escribes varias solicitudes (búsqueda, detalles, quizás más adelante algo más), la misma lógica de verificación de estado comienza a duplicarse. Y cualquier duplicación en el código tiene una propiedad divertida: cuando corriges un error en un lugar, seguramente quedará en otro para que no haya aburrimiento.

Por lo tanto, el siguiente paso correcto es crear un solo lugar donde viva la regla “éxito/fracaso”. Por ahora aún no sacamos todo a un CatalogClient separado (eso será la próxima lección), pero la regla ya se puede centralizar en un método helper.

Introducimos una pequeña excepción para no perder el estado:

public class CatalogApiException extends RuntimeException {
    // Guardamos el código de estado para poder usarlo en el mensaje al usuario/lógica de procesamiento
    private final int status;

    public CatalogApiException(int status, String message) {
        // message aquí es una pista de diagnóstico (por ejemplo, body acortado)
        super(message);
        this.status = status;
    }

    public int status() {
        return status;
    }
}

Y ahora — un método que devuelve el body o lanza CatalogApiException:

import java.net.http.HttpResponse;

private static String require2xx(HttpResponse<String> response) {
    int status = response.statusCode(); // Leemos el estado una vez para no repetirnos
    if (is2xx(status)) {
        // Solo en éxito consideramos el body como "datos"
        return response.body();
    }

    // En error lanzamos una excepción con el estado y diagnóstico (fragmento de body acortado)
    throw new CatalogApiException(status, shorten(response.body(), 200));
}

Observa que este método es casi “matemático”: entrada — HttpResponse<String>, salida — una cadena, pero solo en caso de éxito. Todo lo demás es una excepción. Este estilo reduce drásticamente la cantidad de “verificaciones in situ” y hace el código un poco más lineal: o obtienes datos, o no los obtienes.

Y esto no es una “religión de excepciones”, sino una simple disciplina de transporte: la capa de transporte debe no dejar pasar respuestas erróneas como si fueran datos.

7. En ReadLater Starter: impresión de errores

Ahora juntemos todo en un pequeño escenario, similar a lo que hace nuestra aplicación: enviamos una solicitud, recibimos una respuesta, verificamos el estado, imprimimos el resultado, y si hay error, mostramos un mensaje comprensible.

Esquemáticamente, esto se puede mantener en ReadLaterApplication (o como se llame el punto de entrada) por ahora directamente en la “lógica de comandos”:

import java.io.IOException;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;

try {
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    String body = require2xx(response);

    System.out.println(body); // imprimimos JSON como cadena (por ahora)
} catch (CatalogApiException e) {
    System.out.println(userMessageForStatus(e.status())); // Libro no encontrado en el catálogo
} catch (HttpTimeoutException e) {
    System.out.println("El catálogo responde demasiado lento");
} catch (IOException e) {
    System.out.println("Error de red: " + e.getMessage());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    System.out.println("La solicitud fue interrumpida");
}

Aquí es importante ver el límite: CatalogApiException significa “la respuesta llegó, pero el estado es malo”, mientras que los demás catch significan que no llegamos a una respuesta con la que se pueda trabajar. Es otra rama, otro significado, otro texto.

Si vuelves a juntar esto en un solo catch (Exception), perderás una señal importante: qué sucedió exactamente — la red no dio respuesta o el servidor respondió con rechazo. Y luego te sorprenderás de por qué “todo se ve igual”, aunque las razones son diferentes. Spoiler: porque tú mismo lo escribiste así.

8. Errores típicos con non-2xx

Error #1: considerar cualquier body no vacío como éxito.
Es la trampa más común: el servidor devolvió 404, pero el body contiene JSON con descripción del error, y la mano tiende a parsearlo “como una respuesta normal”. Resultado: o le imprimes al usuario “basura”, o más tarde te topas con un error de deserialización. Hábito correcto: primero el estado, luego el body como datos.

Error #2: verificar solo status == 200.
Mientras haces un solo GET, puede parecer que “así sirve”. Pero el código de transporte es parte del “esqueleto” de la aplicación: debe ser un poco más universal. Verificar 200..299 no es más complicado, pero es mucho más fiable. De lo contrario, te estás sembrando errores en el lugar más inesperado, en cuanto encuentres un 201 o 204 (o una redirección 301 debido a un baseUrl incorrecto).

Error #3: mezclar error de transporte y error HTTP en una misma cosa.
IOException y HttpResponse con estado 500 son historias diferentes. En el primer caso no hay respuesta en absoluto, en el segundo hay respuesta, solo que dice “error”. Si le comunicas al usuario (o a ti mismo) “algo se rompió” en ambos casos de la misma manera, te privas de la oportunidad de entender rápidamente dónde está el problema: en la red o en el servicio remoto.

Error #4: perder el código de estado al formar el mensaje de error.
Si lanzas new IllegalStateException("Error") sin el estado, estás recortando la mitad del significado. El estado es un resumen corto pero muy informativo. Incluso si no sabes analizar el body de forma elegante, el estado ya da una dirección: 404 — “no encontrado”, 429 — “demasiadas veces”, 500 — “allí hay un incendio”.

Error #5: imprimir al usuario un body gigante “tal cual”.
A veces el proveedor devuelve un enorme HTML con trazabilidad o una página de error del proxy. Si imprimes esto sin pensar en la consola, conviertes la salida en un flujo ilegible. Una estrategia normal para un cliente de aprendizaje es limitar la longitud y mostrar solo un fragmento. El body completo, si es necesario, se registrará más tarde con una pila de logging adecuada, no con un “tocho en la consola”.

1
Tarea
Java Server, nivel 15, lección 3
Bloqueada
Éxito por el rango 2xx, no solo por 200
Éxito por el rango 2xx, no solo por 200
1
Tarea
Java Server, nivel 15, lección 3
Bloqueada
Clasificacion de la respuesta del servicio y fallo de transporte
Clasificacion de la respuesta del servicio y fallo de transporte
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION