HTTP Client API
The java.net.http HTTP Client API, finalized as a standard feature in Java 11 (after an incubating preview in Java 9–10), provides a modern, fluent builder-based client for making HTTP requests, supporting both HTTP/1.1 and HTTP/2, synchronous and fully asynchronous (CompletableFuture-based) request execution, and proper streaming request/response bodies — replacing the old java.net.HttpURLConnection, which had been part of the JDK since Java 1.1 and was widely regarded as low-level, awkward to use correctly, and entirely unaware of HTTP/2. This entry covers exactly what was wrong with HttpURLConnection that motivated a replacement, the HttpClient/HttpRequest/HttpResponse builder API and how it's actually used for both sync and async calls, BodyPublishers/BodyHandlers for request and response bodies, and HTTP/2 support including server push.
Motivation — Why HttpURLConnection Needed Replacing
// ── The old way — HttpURLConnection, verbose and error-prone ────────────
URL url = new URL("https://api.example.com/users/1");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(5000);
int status = conn.getResponseCode();
StringBuilder responseBody = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
status >= 200 && status < 300 ? conn.getInputStream() : conn.getErrorStream()))) {
String line;
while ((line = br.readLine()) != null) {
responseBody.append(line);
}
}
conn.disconnect();
// Manual stream handling, manual error-stream-vs-input-stream switching,
// no async option at all, no HTTP/2 awareness whatsoever
// ── The new way — java.net.http.HttpClient, Java 11+ ────────────────────
HttpClient client = HttpClient.newHttpClient(); // HTTP/2 by default, falls back to 1.1 if needed
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users/1"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
// Fluent builder, status code returned directly (no IOException-on-error-status surprise),
// body handling abstracted via BodyHandlers, HTTP/2 negotiated automaticallyBuilding Requests, Synchronous and Asynchronous Execution
// ── Building a reusable client — NOT one client per request ────────────
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // request HTTP/2; falls back to 1.1 automatically
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// Keep and reuse this single "client" instance across the application's lifetime —
// it manages connection pooling internally
// ── Building requests for different HTTP methods ───────────────────────
HttpRequest getReq = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.GET()
.build();
HttpRequest postReq = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"Alice\"}"))
.build();
// ── Synchronous execution — blocks until full response received ────────
HttpResponse<String> syncResponse = client.send(getReq, HttpResponse.BodyHandlers.ofString());
System.out.println(syncResponse.statusCode() + ": " + syncResponse.body());
// ── Asynchronous execution — non-blocking, CompletableFuture-based ──────
CompletableFuture<HttpResponse<String>> futureResponse =
client.sendAsync(getReq, HttpResponse.BodyHandlers.ofString());
futureResponse
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("Got body: " + body))
.exceptionally(ex -> { System.err.println("Request failed: " + ex.getMessage()); return null; });
// Multiple concurrent requests, no manual thread management:
List<CompletableFuture<HttpResponse<String>>> all = List.of(
client.sendAsync(getReq, HttpResponse.BodyHandlers.ofString()),
client.sendAsync(postReq, HttpResponse.BodyHandlers.ofString())
);
CompletableFuture.allOf(all.toArray(new CompletableFuture[0])).join();BodyPublishers, BodyHandlers, and HTTP/2 Server Push
// ── BodyPublishers — constructing request bodies ───────────────────────
HttpRequest jsonPost = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"Bob\"}"))
.build();
HttpRequest fileUpload = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/upload"))
.POST(HttpRequest.BodyPublishers.ofFile(Path.of("large-file.zip"))) // streamed, not fully buffered
.build();
HttpRequest noBodyDelete = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users/1"))
.DELETE() // implicitly uses BodyPublishers.noBody()
.build();
// ── BodyHandlers — choosing how the response body is consumed ──────────
HttpResponse<String> stringResp = client.send(jsonPost, HttpResponse.BodyHandlers.ofString());
HttpResponse<Path> fileResp = client.send(
HttpRequest.newBuilder().uri(URI.create("https://example.com/big-report.pdf")).build(),
HttpResponse.BodyHandlers.ofFile(Path.of("downloaded-report.pdf")) // streamed straight to disk
);
HttpResponse<InputStream> streamResp = client.send(
HttpRequest.newBuilder().uri(URI.create("https://example.com/data")).build(),
HttpResponse.BodyHandlers.ofInputStream() // consume incrementally, e.g. for very large bodies
);
try (InputStream body = streamResp.body()) {
body.transferTo(System.out);
}
HttpResponse<Void> statusOnly = client.send(
HttpRequest.newBuilder().uri(URI.create("https://example.com/ping")).build(),
HttpResponse.BodyHandlers.discarding() // only the status code matters here
);
// ── HTTP/2 server push — handling proactively-pushed resources ─────────
HttpResponse<String> mainResponse = client.send(
HttpRequest.newBuilder().uri(URI.create("https://example.com/page.html")).build(),
HttpResponse.BodyHandlers.ofString()
);
CompletableFuture<HttpResponse<String>> withPush = client.sendAsync(
HttpRequest.newBuilder().uri(URI.create("https://example.com/page.html")).build(),
HttpResponse.BodyHandlers.ofString(),
(pushReq, acceptor) -> {
// Called once per resource the server proactively pushes (e.g. style.css):
CompletableFuture<HttpResponse<String>> pushed =
acceptor.apply(HttpResponse.BodyHandlers.ofString());
pushed.thenAccept(r -> System.out.println("Pushed resource: " + pushReq.uri() + " -> " + r.body().length() + " bytes"));
}
);