📁 I/O, Files & Networking · Intermediate

HttpClient in Java

Java 11+ HTTP client: sync send, sendAsync, BodyHandlers.

🧩 The mysteryYour code calls an API, the server answers "404 Not Found"… and Java throws no exception at all. Bug in the JDK, or a misunderstanding about what "success" means?

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>.

🔮 Predict it

When the server says 404

The server replies 404 Not Found. What does client.send(...) do?

  1. Throws an IOException
  2. Returns a response whose statusCode() is 404
  3. Returns 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

✗ New client each time
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.

✓ One shared client
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.

💼 In the real world

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

  1. Build once, reuse: HttpClient is immutable and thread-safe
  2. send() blocks; sendAsync() returns a CompletableFuture
  3. BodyHandlers.ofString(), ofFile(), ofInputStream()…
  4. A 404 or 500 is a normal response — check statusCode()
🤯 Did you know?

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.___());
  1. ofString
  2. discarding
  3. ofByteArray
  4. 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?

  1. An HttpResponse<String>, after waiting
  2. A CompletableFuture<HttpResponse<String>>
  3. A Thread that is already running
  4. 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.

Next: one level down — raw sockets, the TCP building block under every HTTP call.