Java HttpClient: Modern HTTP Requests in Java 11+
Java 11 introduced java.net.http.HttpClient as a modern replacement for HttpURLConnection. It supports HTTP/1.1 and HTTP/2, synchronous and asynchronous requests, WebSockets, streaming response bodies, and configurable timeouts and redirects. The fluent builder API makes it much cleaner than the old URL-based approach, and CompletableFuture integration enables non-blocking request pipelines.
Contents
Synchronous GET Request
HttpClient.newHttpClient()HttpRequest.newBuilder()send()BodyHandlers.ofString()StringHttpClient
import java.net.http.*;
import java.net.URI;
// Build a shared HttpClient — thread-safe, reuse across requests
HttpClient client = HttpClient.newHttpClient();
// Or with custom config:
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// Build a request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
.header("Accept", "application/json")
.GET() // default, so optional
.build();
// Send synchronously — blocks until response arrives
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode()); // 200
System.out.println("Body: " + response.body()); // JSON string
System.out.println("Headers: " + response.headers().map()); // Map of headers
// Check status and handle errors
if (response.statusCode() == 200) {
String json = response.body();
// parse JSON with Jackson, Gson, etc.
} else {
System.err.println("Error: " + response.statusCode());
}
POST with JSON Body
BodyPublishers.ofString()Content-Typeapplication/jsonHttpResponse.statusCode()HttpClient
import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
// POST with a JSON string body
String jsonBody = """
{
"title": "New Issue",
"body": "Description of the issue",
"labels": ["bug"]
}
""";
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/issues"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = client.send(postRequest, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode()); // 201 Created
System.out.println(response.body());
// PUT request
HttpRequest putRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/issues/42"))
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString("{\"state\": \"closed\"}"))
.build();
// DELETE request
HttpRequest deleteRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/issues/42"))
.DELETE()
.build();
// Form-encoded POST (application/x-www-form-urlencoded)
String formBody = "username=alice&password=secret";
HttpRequest formRequest = HttpRequest.newBuilder()
.uri(URI.create("https://auth.example.com/login"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(formBody))
.build();
Async Requests with CompletableFuture
sendAsync()CompletableFuture<HttpResponse<T>>thenApply()thenAccept()CompletableFuture.allOf()join()allOf
import java.net.http.*;
import java.util.concurrent.CompletableFuture;
import java.util.List;
HttpClient client = HttpClient.newHttpClient();
// sendAsync — returns CompletableFuture, does not block
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(
HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.GET().build(),
HttpResponse.BodyHandlers.ofString()
);
// Process result when done (non-blocking)
future
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("Got: " + body))
.exceptionally(ex -> { System.err.println("Error: " + ex.getMessage()); return null; });
// Fire multiple requests in parallel
List<String> urls = List.of(
"https://api.example.com/users/1",
"https://api.example.com/users/2",
"https://api.example.com/users/3"
);
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> client.sendAsync(
HttpRequest.newBuilder().uri(URI.create(url)).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body))
.toList();
// Wait for all to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> futures.forEach(f -> System.out.println(f.join())))
.join(); // block until all done
HttpClient.newHttpClient()HttpClientTimeouts, Redirects, and Configuration
HttpClient.BuilderconnectTimeoutfollowRedirectsNEVERNORMALALWAYSversionHttpRequest.timeout()ConnectExceptionHttpTimeoutException
import java.time.Duration;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
// Comprehensive client configuration
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // prefer HTTP/2, fallback to HTTP/1.1
.connectTimeout(Duration.ofSeconds(5)) // connection timeout
.followRedirects(HttpClient.Redirect.NORMAL) // follow non-HTTPS-to-HTTP redirects
.authenticator(new Authenticator() { // Basic auth
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user", "pass".toCharArray());
}
})
.build();
// Per-request timeout (overrides client-level timeout)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/slow-endpoint"))
.timeout(Duration.ofSeconds(10)) // request (read) timeout
.header("User-Agent", "MyApp/1.0")
.GET()
.build();
// HttpClient.Redirect options:
// NEVER — never follow redirects
// ALWAYS — always follow (including HTTPS → HTTP, which leaks headers)
// NORMAL — follow except HTTPS → HTTP downgrade (recommended)
try {
HttpResponse<String> resp = client.send(request, HttpResponse.BodyHandlers.ofString());
// response.uri() — final URI after redirects
System.out.println("Final URL: " + resp.uri());
} catch (java.net.http.HttpTimeoutException e) {
System.err.println("Request timed out");
} catch (java.net.ConnectException e) {
System.err.println("Cannot connect: " + e.getMessage());
}
Body Handlers and Publishers
BodyHandlersofString()StringofBytes()byte[]ofFile(path)ofInputStream()ofLines()Stream<String>discarding()BodyPublishersofString()ofByteArray()ofFile()noBody()
// BodyHandlers — control how response body is consumed
HttpResponse.BodyHandler<?> handler;
// ofString() — full body as String (all in memory)
// ofInputStream() — raw InputStream for streaming large responses
// ofByteArray() — body as byte[]
// ofFile(Path) — stream body to a file
// ofLines() — Stream<String> of lines (lazy)
// discarding() — discard body (only need status/headers)
// Stream large response to a file
HttpResponse<Path> fileResp = client.send(
HttpRequest.newBuilder().uri(URI.create("https://example.com/large.zip")).GET().build(),
HttpResponse.BodyHandlers.ofFile(Path.of("downloaded.zip"))
);
System.out.println("Saved to: " + fileResp.body());
// Stream lines lazily
HttpResponse<Stream<String>> linesResp = client.send(
HttpRequest.newBuilder().uri(URI.create("https://example.com/log")).GET().build(),
HttpResponse.BodyHandlers.ofLines()
);
try (Stream<String> lines = linesResp.body()) {
lines.filter(l -> l.contains("ERROR")).forEach(System.out::println);
}
// BodyPublishers — control how request body is sent
HttpRequest.BodyPublisher pub;
HttpRequest.BodyPublishers.ofString("hello"); // from String
HttpRequest.BodyPublishers.ofByteArray(bytes); // from byte[]
HttpRequest.BodyPublishers.ofFile(Path.of("data.json")); // from file
HttpRequest.BodyPublishers.noBody(); // for GET/DELETE