Java HTTP/2 Client: From Blocking to Asynchronous

Sanjeev Kumar Rai
4 min readFeb 21, 2021

An HttpClient can be used to access any resource on the web via HTTP.

prior to Java 11, developers had to use legacy class HttpUrlConnection which is considered to be more abstract or use third-part library such as Apache HttpClient, or OkHttp.

From JDK11, It supports HTTP/1.1 and HTTP/2, both synchronous and asynchronous programming models, handles request and response bodies as reactive-streams, and follows the familiar builder pattern. By default the client will send requests using HTTP/2. Requests sent to servers that do not yet support HTTP/2 will automatically be downgraded to HTTP/1.1.

The new API is now providing non-blocking request and response handling by CompletableFutures. The other concepts, like back-pressure and flow-control, has been provided by reactive streams through java.uti.consurrent.Flow API.

Lets dive in for examples and recipes that can be followed to perform common tasks using the Java HTTP Client.

Synchronous Get

Response body as a String

public void get(String uri) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println(response.body());
}

Response body as a File

public void get(String uri) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();
HttpResponse<Path> response =
client.send(request, BodyHandlers.ofFile(Paths.get("body.txt")));
System.out.println("Response in file:" + response.body());
}

Asynchronous Get

The asynchronous API returns immediately with a CompletableFuture that completes with the HttpResponse when it becomes available. CompletableFuture was added in Java 8 and supports composable asynchronous programming.

Response body as a String

public CompletableFuture<String> get(String uri) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();
return client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}

The CompletableFuture.thenApply(Function) method can be used to map the HttpResponse to its body type, status code, etc.

Post

A request body can be supplied by an HttpRequest.BodyPublisher.

public void post(String uri, String data) throws Exception {
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.POST(BodyPublishers.ofString(data))
.build();
HttpResponse<?> response = client.send(request, BodyHandlers.discarding());
System.out.println(response.statusCode());
}

The above example uses the ofString BodyPublisher to convert the given String into request body bytes.

The BodyPublisher is a reactive-stream publisher that publishes streams of request body on-demand. HttpRequest.Builder has a number of methods that allow setting a BodyPublisher; Builder::POST, Builder::PUT, and Builder::method. The HttpRequest.BodyPublishers class has a number of convenience static factory methods that create a BodyPublisher for common types of data; ofString, ofByteArray, ofFile.

The discarding BodyHandler can be used to receive and discard the response body when it is not of interest.

Concurrent Requests

It’s easy to combine Java Streams and the CompletableFuture API to issue a number of requests and await their responses. The following example sends a GET request for each of the URIs in the list and stores all the responses as Strings.

public void getURIs(List<URI> uris) {
HttpClient client = HttpClient.newHttpClient();
List<HttpRequest> requests = uris.stream()
.map(HttpRequest::newBuilder)
.map(reqBuilder -> reqBuilder.build())
.collect(toList());
CompletableFuture.allOf(requests.stream()
.map(request -> client.sendAsync(request, ofString()))
.toArray(CompletableFuture<?>[]::new))
.join();
}

Get JSON

In many cases the response body will be in some higher-level format. The convenience response body handlers can be used, along with a third-party library to convert the response body into that format.

The following example demonstrates how to use the Jackson library, in combination with BodyHandlers::ofString to convert a JSON response into a Map of String key/value pairs.

public CompletableFuture<Map<String,String>> JSONBodyAsMap(URI uri) {
UncheckedObjectMapper objectMapper = new UncheckedObjectMapper();
HttpRequest request = HttpRequest.newBuilder(uri)
.header("Accept", "application/json")
.build();
return HttpClient.newHttpClient()
.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenApply(objectMapper::readValue);
}
class UncheckedObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
/** Parses the given JSON string into a Map. */
Map<String,String> readValue(String content) {
try {
return this.readValue(content, new TypeReference<>(){});
} catch (IOException ioe) {
throw new CompletionException(ioe);
}
}

The above example uses ofString which accumulates the response body bytes in memory. Alternatively, a streaming subscriber, like ofInputStream could be used.

Post JSON

In many cases the request body will be in some higher-level format. The convenience request body handlers can be used, along with a third-party library to convert the request body into that format.

The following example demonstrates how to use the Jackson library, in combination with the BodyPublishers::ofString to convert a Map of String key/value pairs into JSON.

public CompletableFuture<Void> postJSON(URI uri,
Map<String,String> map)
throws IOException
{
ObjectMapper objectMapper = new ObjectMapper();
String requestBody = objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(map);
HttpRequest request = HttpRequest.newBuilder(uri)
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(requestBody))
.build();
return HttpClient.newHttpClient()
.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::statusCode)
.thenAccept(System.out::println);
}

Setting a Proxy

A ProxySelector can be configured on the HttpClient through the client’s Builder::proxy method. The ProxySelector API returns a specific proxy for a given URI. In many cases a single static proxy is sufficient. The ProxySelector::of static factory method can be used to create such a selector.

Response body as a String with a specified proxy

public CompletableFuture<String> get(String uri) {
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("www-proxy.com", 8080)))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();
return client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}

Alternatively, the system-wide default proxy selector can be used, which is the default on macOS.

HttpClient.newBuilder()
.proxy(ProxySelector.getDefault())
.build();

--

--