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 clienteHttpClient.newBuilder()...build(), una sola vez
Mandar un cuerpoBodyPublishers.ofString / ofFile
Recibir textoBodyHandlers.ofString()
Bajar un archivo grandeBodyHandlers.ofFile(ruta)
Saber si salió bienr.statusCode(), siempre
Una cabecera de la respuestar.headers().firstValue("...")
No bloquearsendAsync, 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.