Contents

HttpClient.newHttpClient() creates a client with sensible defaults (HTTP/2 preferred, no redirect following). HttpRequest.newBuilder() configures the request with a URI, HTTP method, headers, and an optional per-request timeout. Calling send() blocks the calling thread until the full response arrives. BodyHandlers.ofString() decodes the response body into a String using the charset indicated in the Content-Type header. The HttpClient is thread-safe and should be created once and reused across requests to benefit from connection pooling.

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()); }

BodyPublishers.ofString() sends a string as the request body, encoded as UTF-8 by default. You must explicitly set the Content-Type header to application/json — the client does not infer it from the body. After sending, check HttpResponse.statusCode() for a 2xx response before processing the body. For deserializing the JSON response into a Java object, pass the body string to a JSON library such as Jackson or Gson; there is no built-in JSON binding in 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();

sendAsync() submits the request and returns a CompletableFuture<HttpResponse<T>> immediately without blocking the calling thread. Chain thenApply() to transform the response and thenAccept() to consume it. To fire multiple requests in parallel, create a list of futures and use CompletableFuture.allOf() to wait for all of them; call join() on each future to retrieve the result once allOf completes. This pattern allows N HTTP calls to execute concurrently without blocking N threads.

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() should be created once and reused — it manages a connection pool internally. Creating a new HttpClient per request defeats connection reuse and hurts performance.

HttpClient.Builder centralises client-level settings that apply to all requests: connectTimeout limits how long to wait for a TCP connection; followRedirects accepts NEVER, NORMAL (follow except HTTPS-to-HTTP downgrades), or ALWAYS; version sets the preferred HTTP version. A per-request timeout can be set via HttpRequest.timeout() and applies to the total time waiting for a response. Connection timeouts throw ConnectException; request timeouts throw HttpTimeoutException — catch them separately for meaningful error handling.

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()); }

BodyHandlers controls how the response body is consumed: ofString() loads it entirely into memory as a String; ofBytes() loads it as a byte[]; ofFile(path) streams it directly to disk; ofInputStream() gives raw streaming access; ofLines() returns a lazy Stream<String>; discarding() drops the body entirely when only the status code or headers matter. BodyPublishers mirrors this for request bodies: ofString(), ofByteArray(), ofFile(), and noBody() for requests with no body such as GET or DELETE.

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