HttpClient in Java
Java 11+ HTTP client: sync send, sendAsync, BodyHandlers.
Three objects
Java 11's **java.net.http.HttpClient speaks HTTP/1.1 and HTTP/2. HttpClient: reusable, immutable and thread-safe. HttpRequest: an immutable URI, method and headers, built with a builder. HttpResponse**: status code, headers and body.
send: synchronous
**client.send(request, bodyHandler) blocks** until the response arrives and returns an HttpResponse.
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(
URI.create("https://example.com"))
.build();
HttpResponse<String> res = client.send(
req, BodyHandlers.ofString());
int status = res.statusCode();BodyHandlers pick the body type
The BodyHandler decides what the body becomes: **ofString()** → String, **ofByteArray()** → byte[], **ofLines()** → Stream<String>, **ofFile(path) → saved to disk, ofInputStream() → streamed, discarding()** → Void. It must match the declared HttpResponse<T>.
When the server says 404
The server replies 404 Not Found. What does client.send(...) do?
Throws an IOExceptionReturns a response whose statusCode() is 404Returns null
Show the answer
It returns normally — any HTTP status is a completed exchange. send only throws for I/O problems (or interruption). **Check statusCode()** yourself.
sendAsync: don't wait
**sendAsync returns immediately with a CompletableFuture<HttpResponse<T>>**. Attach callbacks, or join() later.
client.sendAsync(req, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);Sending 10,000 requests
for (URI uri : uris) {
var client = HttpClient.newHttpClient();
client.send(HttpRequest
.newBuilder(uri).build(),
BodyHandlers.ofString());
}Each client has its own connection pool and threads — nothing is reused.
var client = HttpClient.newHttpClient();
for (URI uri : uris) {
client.send(HttpRequest
.newBuilder(uri).build(),
BodyHandlers.ofString());
}Pooled connections stay alive across requests; thread-safe, so share it.
Calling APIs for real
Microservices call each other over HTTP all day. Production code shares one client, sets timeouts, checks status codes (a 500 is not an exception!), and uses sendAsync or virtual threads to make many calls concurrently.
Key takeaways
- Build once, reuse: HttpClient is immutable and thread-safe
- send() blocks; sendAsync() returns a CompletableFuture
- BodyHandlers.ofString(), ofFile(), ofInputStream()…
- A 404 or 500 is a normal response — check statusCode()
Before Java 11, the JDK's only built-in HTTP API was HttpURLConnection from 1997 — clunky enough that most projects added Apache HttpClient or OkHttp instead.
Practice questions
Read the response body as text.
HttpResponse<String> res = client.send(
request,
HttpResponse.BodyHandlers.___());- ofString
- discarding
- ofByteArray
- ofLines
Check your answer
ofString. BodyHandlers.ofString() produces an HttpResponse<String>. discarding gives Void, ofByteArray gives byte[], and ofLines gives Stream<String> — none match the declared type.
What does client.sendAsync(request, BodyHandlers.ofString()) return?
- An HttpResponse<String>, after waiting
- A CompletableFuture<HttpResponse<String>>
- A Thread that is already running
- A Stream<String> of the body
Check your answer
A CompletableFuture<HttpResponse<String>>. sendAsync returns immediately with a CompletableFuture; you attach callbacks like thenApply(HttpResponse::body) or call join() later.