Peticiones HTTP en Java: GET y POST

Desde Java 11 hay un cliente HTTP en la biblioteca estándar, con HTTP/2 y peticiones asíncronas. Ya no hace falta añadir ninguna dependencia para hablar con una API.

El cliente

HttpClient cliente = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build();

Se crea una vez y se reutiliza: mantiene el grupo de conexiones y la configuración de HTTP/2. Crear uno por petición es el error de rendimiento más común con esta API.

GET

HttpRequest get = HttpRequest.newBuilder()
        .uri(URI.create("https://httpbin.org/get?lenguaje=java&version=25"))
        .header("Accept", "application/json")
        .header("User-Agent", "decodigo-ejemplo/1.0")
        .timeout(Duration.ofSeconds(20))
        .GET()
        .build();

HttpResponse<String> r = cliente.send(get, HttpResponse.BodyHandlers.ofString());
salidaestado   : 200
  versión  : HTTP_2
  tipo     : application/json
  cuerpo   : "lenguaje": "java",

Negoció HTTP/2 sin que se lo pidiéramos. Hay dos tiempos de espera distintos y conviene poner los dos: el connectTimeout del cliente, para establecer la conexión, y el timeout de la petición, para la respuesta completa.

POST

String json = """
        {"nombre": "decodigo", "lenguaje": "java"}""";

HttpRequest post = HttpRequest.newBuilder()
        .uri(URI.create("https://httpbin.org/post"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();
salidaeco      : "data": "{\"nombre\": \"decodigo\", \"lenguaje\": \"java\"}",

Hay un BodyPublisher para cada caso: ofString, ofFile, ofByteArray y noBody. Y al otro lado, un BodyHandler: ofString, ofLines, ofInputStream y ofFile, que descarga directamente a disco sin cargar nada en memoria.

Un 404 no es una excepción

salidaestado   : 404   ¿hay excepción? no
Esto sorprende a casi todo el mundo. send solo lanza si la petición no llegó a completarse —no hay red, no resuelve el nombre, venció el plazo—. Un 404, un 500 o un 403 son respuestas perfectamente válidas desde el punto de vista del cliente. Hay que mirar statusCode() siempre, o acabarás analizando una página de error como si fuera JSON.

Asíncrono

var futuro = cliente.sendAsync(get, HttpResponse.BodyHandlers.ofString())
                    .thenApply(HttpResponse::statusCode);
IO.println("mientras tanto el programa sigue…");
IO.println("llegó: " + futuro.get());
salidamientras tanto el programa sigue…
  llegó: 200

sendAsync devuelve un CompletableFuture y no bloquea. Para lanzar muchas peticiones a la vez hay dos caminos: encadenar futuros, o —más legible desde Java 21— usar send normal dentro de hilos virtuales, que bloquean sin coste.

Resumen

Para… Se usa
Crear el cliente HttpClient.newBuilder()...build(), una sola vez
Mandar un cuerpo BodyPublishers.ofString / ofFile
Recibir texto BodyHandlers.ofString()
Bajar un archivo grande BodyHandlers.ofFile(ruta)
Saber si salió bien r.statusCode(), siempre
Una cabecera de la respuesta r.headers().firstValue("...")
No bloquear sendAsync, o send en un hilo virtual

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.