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 anint(e.g.200,404). There is noisSuccessful(); check the code yourself.body()— the body, already converted toTby theBodyHandleryou passed.headers()— anHttpHeaders. UsefirstValue("Content-Type")(returns anOptional<String>) orallValues("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.
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
HttpURLConnection,HttpClientis 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,
HttpClientreturns the response normally for any status it received; only transport failures (connection refused, timeout, DNS) throwIOException. Always inspectstatusCode()— the body of an error response is still readable. - Build one client, share it.
HttpClientis immutable and thread-safe and owns its connection pool. Creating one per request throws away connection reuse and HTTP/2 sessions. There is noclose()to call before Java 21 (and on Java 21+ it isAutoCloseable, but a long-lived shared client rarely needs closing). connectTimeoutandtimeoutare 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 anHttpTimeoutException.- A
GETcannot carry a body. CallingGET()with aBodyPublisheris not how the API works — usePOST,PUT, or the genericmethod(name, publisher)for verbs that send data. URI.createmust receive an absolute, well-formed URI. Spaces and other unsafe characters are not encoded for you; encode query parameters before building the URI.