FeatureDetail
ProtocolsHTTP/1.1 and HTTP/2 (with automatic upgrade)
Execution modelsSynchronous (send()) and async (sendAsync())
Thread modelUses a shared ForkJoinPool or custom Executor
TLSConfigurable SSLContext and SSLParameters
Redirect policyNEVER, ALWAYS, NORMAL (no HTTP→HTTPS downgrade)
AuthenticationPluggable Authenticator for Basic/Digest
WebSocketBuilt-in newWebSocketBuilder()
Connection reuseAutomatic keep-alive and H2 multiplexing

Build a shared, reusable client instance — do not create one per request.

import java.net.http.*; import java.time.Duration; HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) // prefer HTTP/2 .connectTimeout(Duration.ofSeconds(10)) .followRedirects(HttpClient.Redirect.NORMAL) .build(); // Or use the default singleton (HTTP_2, NEVER redirect, no timeout) HttpClient defaultClient = HttpClient.newHttpClient(); HttpClient is thread-safe and intended to be shared. Each instance manages its own connection pool. Create one per application, not one per request.
import java.net.URI; import java.net.http.*; 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("Status : " + response.statusCode()); System.out.println("Body : " + response.body()); System.out.println("Headers: " + response.headers().map()); String json = """ {"name": "Alice", "email": "alice@example.com"} """; HttpRequest post = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/users")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> response = client.send(post, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); // 201 Created // PUT — update resource HttpRequest put = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/users/1")) .header("Content-Type", "application/json") .PUT(HttpRequest.BodyPublishers.ofString(json)) .build(); // DELETE HttpRequest delete = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/users/1")) .DELETE() .build();

sendAsync() returns a CompletableFuture — the calling thread is not blocked.

HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/data")) .build(); CompletableFuture<String> future = client .sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body); // Fan-out: send 5 requests concurrently List<URI> uris = List.of( URI.create("https://api.example.com/a"), URI.create("https://api.example.com/b"), URI.create("https://api.example.com/c") ); List<CompletableFuture<String>> futures = uris.stream() .map(uri -> HttpRequest.newBuilder(uri).build()) .map(req -> client.sendAsync(req, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body)) .toList(); // Wait for all to complete CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); futures.forEach(f -> System.out.println(f.join()));

BodyHandlers — how to consume the response body:

HandlerReturns
ofString()String decoded with response charset
ofByteArray()Raw byte[]
ofFile(path)Saves body to a file, returns Path
ofInputStream()InputStream for streaming reads
ofLines()Stream<String> — line-by-line streaming
discarding()Ignores the body (useful for HEAD / fire-and-forget)

BodyPublishers — how to supply the request body:

PublisherSource
ofString(s)String
ofByteArray(b)byte[]
ofFile(path)File contents
ofInputStream(supplier)InputStream (lazily created)
noBody()Empty body (GET, DELETE)
// Stream a large file upload HttpRequest upload = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/upload")) .header("Content-Type", "application/octet-stream") .POST(HttpRequest.BodyPublishers.ofFile(Path.of("/data/large.bin"))) .build(); // Stream response as lines (SSE-like) HttpResponse<Stream<String>> lines = client.send( HttpRequest.newBuilder(URI.create("https://stream.example.com/events")).build(), HttpResponse.BodyHandlers.ofLines()); lines.body().forEach(System.out::println);
// Bearer token — add Authorization header manually HttpRequest bearerRequest = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/secure")) .header("Authorization", "Bearer " + token) .build(); // Basic auth via Authenticator (handles 401 challenges automatically) HttpClient authClient = HttpClient.newBuilder() .authenticator(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("user", "pass".toCharArray()); } }) .build();

Java's HttpClient can upgrade an HTTP connection to WebSocket with newWebSocketBuilder().

import java.net.http.WebSocket; WebSocket ws = client.newWebSocketBuilder() .buildAsync(URI.create("wss://echo.websocket.org"), new WebSocket.Listener() { @Override public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) { System.out.println("Received: " + data); ws.request(1); // request next message return null; } @Override public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) { System.out.println("Closed: " + statusCode + " " + reason); return null; } @Override public void onError(WebSocket ws, Throwable error) { error.printStackTrace(); } }) .join(); ws.sendText("Hello WebSocket!", true).join(); // true = last fragment ws.sendClose(WebSocket.NORMAL_CLOSURE, "done").join(); Always call ws.request(n) after processing a message. The WebSocket API uses a demand model — it will not deliver more messages until you request them.

When HttpClient.Version.HTTP_2 is set and the server supports it, the client negotiates H2 via ALPN (TLS) or the HTTP Upgrade header. Multiple requests share a single TCP connection without head-of-line blocking.

// Verify which protocol version was used HttpResponse<String> r = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(r.version()); // HTTP_2 or HTTP_1_1 // Custom executor for async responses HttpClient h2Client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .executor(Executors.newFixedThreadPool(4)) .build(); HTTP/2 server push was removed in Java 18 (JEP 418). Push promise frames are now rejected with a RST_STREAM. Use standard request-response instead.