Java HttpClient
Make HTTP requests in modern Java with java.net.http.HttpClient, including sync, async, and HTTP/2 support.
Java HttpClient
java.net.http.HttpClient, standardised in Java 11, is the modern HTTP API and the one to reach for in new code. It replaces the verbose HttpURLConnection with an immutable, builder-based design: HTTP/2 by default, native synchronous and asynchronous calls, and pluggable handling of request and response bodies. Three types do the work — HttpClient, HttpRequest, and HttpResponse.
The three types
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();A single HttpClient is thread-safe and reusable — build one and share it across the whole application; do not create one per request. From it you send HttpRequest objects and get back HttpResponse objects.
Building a request
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();The verb method (GET(), POST(...), PUT(...), DELETE()) is chosen on the builder. A BodyPublisher supplies the request body — ofString, ofByteArray, ofFile, or noBody(). Requests are immutable once built and can be reused.
Sending: synchronous and asynchronous
// 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());A BodyHandler decides how the response body is materialised: ofString(), ofByteArray(), ofFile(path), ofLines() (a Stream<String>), or discarding(). sendAsync returns a CompletableFuture, so you chain .thenApply, .thenAccept, and .exceptionally without blocking a thread.
A worked example: sync GET, sync POST, and async
This program serves a loopback endpoint that reports the HTTP method it received, then drives it three ways with one shared HttpClient: a synchronous GET, a synchronous POST with a body, and an asynchronous GET through a CompletableFuture.
What to take from the run:
- One
HttpClientserved all three requests. The client is immutable and thread-safe, so the right pattern is build-once-share-everywhere; spinning up a client per call wastes connection pools and HTTP/2 sessions. Notice nodisconnect()anywhere — the client manages connections for you. - The request verb lived on the builder:
.GET()for the read and.POST(BodyPublishers.ofString("payload"))for the write. The server echoed back the method it saw (handled GET,handled POST), confirming the publisher both carried the body and set the verb. HttpResponseis a typed object withstatusCode()andbody(). Because aBodyHandlers.ofString()was passed, the body came back already decoded as aString— swap inofByteArray,ofFile, orofLinesand the same call yields bytes, a saved file, or a stream of lines.- The asynchronous call returned a
CompletableFutureand composed with.thenApplywithout blocking a thread untilfuture.get(). That is the structural win overHttpURLConnection: concurrency is built in, so hundreds of in-flight requests need not mean hundreds of parked threads. - The whole flow — client, request, sync and async sends, typed responses — used no manual streams and no error-stream trap. Compared with the previous chapter,
HttpClientis shorter, safer, and more capable, which is why it is the default choice on Java 11+.
Practice
In a high-traffic service, a developer writes 'HttpClient.newHttpClient()' inside the method that handles each incoming request, creating a fresh client per call. Reviewers flag it. Why?