Java HttpClient
Realiza solicitudes HTTP en Java moderno con java.net.http.HttpClient, incluyendo soporte síncrono, asíncrono y HTTP/2.
java.net.http.HttpClient, estandarizado en Java 11, es la API HTTP moderna y la que se debe utilizar en código nuevo. Reemplaza al verboso HttpURLConnection con un diseño inmutable basado en constructores: HTTP/2 por defecto, llamadas síncronas y asíncronas nativas, y manejo conectable de cuerpos de solicitud y respuesta. Tres tipos hacen el trabajo — HttpClient, HttpRequest y HttpResponse.
Este capítulo cubre la construcción y reutilización de un cliente, la construcción de solicitudes con cuerpos y encabezados, los modos de envío síncrono y asíncrono, cómo los BodyHandlers convierten una respuesta en el tipo que deseas, y los problemas que afectan a los usuarios nuevos. Si eres nuevo en el networking de Java, comienza con la introducción al networking; para el bloque de construcción asíncrono utilizado aquí, consulta CompletableFuture.
Los tres tipos
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // HTTP/2, falling back to 1.1
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();Un único HttpClient es seguro para hilos y reutilizable — construye uno y compártelo en toda la aplicación; no crees uno por solicitud. A partir de él envías objetos HttpRequest y recibes objetos HttpResponse.
Construyendo una solicitud
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString("{\"x\":1}"))
.build();El método de verbo (GET(), POST(...), PUT(...), DELETE()) se elige en el constructor. Un BodyPublisher suministra el cuerpo de la solicitud — ofString, ofByteArray, ofFile o noBody(). Las solicitudes son inmutables una vez construidas y pueden reutilizarse.
Envío: síncrono y asíncrono
// Blocking
HttpResponse<String> resp =
client.send(request, HttpResponse.BodyHandlers.ofString());
// Non-blocking — returns a CompletableFuture
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString());Un BodyHandler decide cómo se materializa el cuerpo de la respuesta: ofString(), ofByteArray(), ofFile(path), ofLines() (un Stream<String>) o discarding(). sendAsync devuelve un CompletableFuture, por lo que puedes encadenar .thenApply, .thenAccept y .exceptionally sin bloquear un hilo.
Leyendo la respuesta
HttpResponse<T> es un objeto de valor tipado simple. Los métodos más utilizados:
statusCode()— el estado HTTP como unint(por ejemplo,200,404). No existeisSuccessful(); comprueba el código tú mismo.body()— el cuerpo, ya convertido aTpor elBodyHandlerque pasaste.headers()— unHttpHeaders. UsafirstValue("Content-Type")(devuelve unOptional<String>) oallValues("Set-Cookie")para encabezados repetidos.uri()— el URI final, que puede diferir del URI de la solicitud después de una redirección.
Los nombres de encabezado se comparan sin distinción entre mayúsculas y minúsculas, por lo que firstValue("content-type") y firstValue("Content-Type") devuelven el mismo valor.
Un ejemplo completo: GET síncrono, POST síncrono y asíncrono
Este programa sirve un endpoint de loopback que reporta el método HTTP que recibió, y luego lo acciona de tres maneras con un único HttpClient compartido: un GET síncrono, un POST síncrono con cuerpo, y un GET asíncrono a través de un CompletableFuture.
Lo que hay que destacar de la ejecución:
- Un único
HttpClientsirvió las tres solicitudes. El cliente es inmutable y seguro para hilos, por lo que el patrón correcto es construir-una-vez-compartir-en-todas-partes; crear un cliente por llamada desperdicia grupos de conexiones y sesiones HTTP/2. Observa que no hay ningúndisconnect()en ningún lugar — el cliente gestiona las conexiones por ti. - El verbo de la solicitud vivía en el constructor:
.GET()para la lectura y.POST(BodyPublishers.ofString("payload"))para la escritura. El servidor devolvió el método que recibió (handled GET,handled POST), confirmando que el publisher tanto llevó el cuerpo como estableció el verbo. HttpResponsees un objeto tipado constatusCode()ybody(). Dado que se pasó unBodyHandlers.ofString(), el cuerpo regresó ya decodificado como unString— intercambiaofByteArray,ofFileuofLinesy la misma llamada produce bytes, un archivo guardado o un stream de líneas.- La llamada asíncrona devolvió un
CompletableFuturey se compuso con.thenApplysin bloquear un hilo hastafuture.get(). Esa es la ventaja estructural sobreHttpURLConnection: la concurrencia está integrada, por lo que cientos de solicitudes en vuelo no necesitan significar cientos de hilos aparcados. - Todo el flujo — cliente, solicitud, envíos síncrono y asíncrono, respuestas tipadas — no utilizó flujos manuales ni la trampa del flujo de error. Comparado con
HttpURLConnection,HttpClientes más corto, más seguro y más capaz, razón por la cual es la opción predeterminada en Java 11+.
Problemas comunes
- Un 4xx o 5xx no es una excepción. A diferencia de algunas bibliotecas,
HttpClientdevuelve la respuesta normalmente para cualquier estado que recibió; solo los fallos de transporte (conexión rechazada, tiempo de espera, DNS) lanzanIOException. Siempre inspeccionastatusCode()— el cuerpo de una respuesta de error sigue siendo legible. - Construye un cliente, compártelo.
HttpClientes inmutable y seguro para hilos y posee su grupo de conexiones. Crear uno por solicitud descarta la reutilización de conexiones y las sesiones HTTP/2. No hay ningúnclose()que llamar antes de Java 21 (y en Java 21+ esAutoCloseable, pero un cliente compartido de larga duración raramente necesita cerrarse). connectTimeoutytimeoutson diferentes.HttpClient.connectTimeout(...)limita cuánto tiempo puede tardar en establecerse la conexión TCP;HttpRequest.timeout(...)limita toda la solicitud/respuesta. Una solicitud que agota el tiempo de espera completa el futuro de forma excepcional con unaHttpTimeoutException.- Un
GETno puede llevar un cuerpo. Llamar aGET()con unBodyPublisherno es como funciona la API — usaPOST,PUTo el genéricomethod(name, publisher)para verbos que envían datos. URI.createdebe recibir un URI absoluto y bien formado. Los espacios y otros caracteres no seguros no se codifican por ti; codifica los parámetros de consulta antes de construir el URI.