W3docs

Java HttpClient

Make HTTP requests in modern Java with java.net.http.HttpClient, including sync, async, and HTTP/2 support.

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.

This chapter covers building and reusing a client, constructing requests with bodies and headers, the synchronous and asynchronous send modes, how BodyHandlers turn a response into the type you want, and the gotchas that bite first-time users. If you are new to Java networking, start with the networking introduction; for the async building block used here, see CompletableFuture.

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.

Reading the response

HttpResponse<T> is a plain typed value object. The methods you reach for most:

  • statusCode() — the HTTP status as an int (e.g. 200, 404). There is no isSuccessful(); check the code yourself.
  • body() — the body, already converted to T by the BodyHandler you passed.
  • headers() — an HttpHeaders. Use firstValue("Content-Type") (returns an Optional<String>) or allValues("Set-Cookie") for repeated headers.
  • uri() — the final URI, which may differ from the request URI after a redirect.

Header names are matched case-insensitively, so firstValue("content-type") and firstValue("Content-Type") return the same value.

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 HttpURLConnection, HttpClient is shorter, safer, and more capable, which is why it is the default choice on Java 11+.

Common gotchas

  • A 4xx or 5xx is not an exception. Unlike some libraries, HttpClient returns the response normally for any status it received; only transport failures (connection refused, timeout, DNS) throw IOException. Always inspect statusCode() — the body of an error response is still readable.
  • Build one client, share it. HttpClient is immutable and thread-safe and owns its connection pool. Creating one per request throws away connection reuse and HTTP/2 sessions. There is no close() to call before Java 21 (and on Java 21+ it is AutoCloseable, but a long-lived shared client rarely needs closing).
  • connectTimeout and timeout are different. HttpClient.connectTimeout(...) caps how long establishing the TCP connection may take; HttpRequest.timeout(...) caps the whole request/response. A request that times out completes the future exceptionally with an HttpTimeoutException.
  • A GET cannot carry a body. Calling GET() with a BodyPublisher is not how the API works — use POST, PUT, or the generic method(name, publisher) for verbs that send data.
  • URI.create must receive an absolute, well-formed URI. Spaces and other unsafe characters are not encoded for you; encode query parameters before building the URI.

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?
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?
Was this page helpful?