W3docs

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.

java— editable, runs on the server

What to take from the run:

  • One HttpClient served 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 no disconnect() 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.
  • HttpResponse is a typed object with statusCode() and body(). Because a BodyHandlers.ofString() was passed, the body came back already decoded as a String — swap in ofByteArray, ofFile, or ofLines and the same call yields bytes, a saved file, or a stream of lines.
  • The asynchronous call returned a CompletableFuture and composed with .thenApply without blocking a thread until future.get(). That is the structural win over HttpURLConnection: 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, HttpClient is shorter, safer, and more capable, which is why it is the default choice on Java 11+.

Practice

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?