Do you remember HttpUrlConnection? Well, JDK 11 has reinvented this API as the HTTP Client API.

Do you remember HttpUrlConnection ? Well, JDK 11 comes up with the HTTP Client API as a reinvention of HttpUrlConnection . The HTTP Client API is easy to use and supports HTTP/2 (default) and HTTP/1.1. For backward compatibility, the HTTP Client API will automatically downgrade from HTTP/2 to HTTP 1.1 when the server doesn't support HTTP/2.

Moreover, the HTTP Client API supports synchronous and asynchronous programming models and relies on streams to transfer data (reactive streams). It also supports the WebSocket protocol, which is used in real-time web applications to provide client-server communication with low message overhead.

You may also like: Java 11: Standardized HTTP Client API

Besides multiplexing, another powerful feature of HTTP/2 is its server push capability. Mainly, in the traditional approach (HTTP/1.1), a browser triggers a request for getting an HTML page and parses the received markup to identify the referenced resources (for example, JS, CSS, images, and so on).

To fetch these resources, the browser sends additional requests (one request for each referenced resource). On the other hand, HTTP/2 sends the HTML page and the referenced resources without explicit requests from the browser. So, the browser requests the HTML page and receives the page and everything else that's needed for displaying the page. The HTTP Client API supports this HTTP/2 feature via the PushPromiseHandler interface.

The implementation of this interface must be given as the third argument of the send() or sendAsync() method. PushPromiseHandler relies on three coordinates, as follows:

The initiating client send request ( initiatingRequest )

The synthetic push request ( pushPromiseRequest )

The acceptor function, which must be successfully invoked to accept the push promise (acceptor)

A push promise is accepted by invoking the given acceptor function. The acceptor function must be passed a non-null BodyHandler , which is used to handle the promise's response body. The acceptor function will return a CompletableFuture instance that completes the promise's response.

Based on this information, let's look at an implementation of PushPromiseHandler :

private static final List<CompletableFuture<Void>> asyncPushRequests = new CopyOnWriteArrayList<>(); ... private static HttpResponse.PushPromiseHandler<String> pushPromiseHandler() { return (HttpRequest initiatingRequest, HttpRequest pushPromiseRequest, Function<HttpResponse.BodyHandler<String> , CompletableFuture<HttpResponse<String>>> acceptor) -> { CompletableFuture<Void> pushcf = acceptor.apply(HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept((b) -> System.out.println( "

Pushed resource body:

" + b)); asyncPushRequests.add(pushcf); System.out.println("

Just got promise push number: " + asyncPushRequests.size()); System.out.println("

Initial push request: " + initiatingRequest.uri()); System.out.println("Initial push headers: " + initiatingRequest.headers()); System.out.println("Promise push request: " + pushPromiseRequest.uri()); System.out.println("Promise push headers: " + pushPromiseRequest.headers()); }; }





Now, let's trigger a request and pass this PushPromiseHandler to sendAsync() :

HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://http2.golang.org/serverpush")) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofString(), pushPromiseHandler()) .thenApply(HttpResponse::body) .thenAccept((b) -> System.out.println("

Main resource:

" + b)) .join(); asyncPushRequests.forEach(CompletableFuture::join); System.out.println("

Fetched a total of " + asyncPushRequests.size() + " push requests");





The complete source code is available on GitHub.

If we want to return a push promise handler that accumulates push promises, and their responses, into the given map, then we can rely on the PushPromiseHandler.of() method, as follows:

private static final ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<String>>> promisesMap = new ConcurrentHashMap<>(); private static final Function<HttpRequest, HttpResponse.BodyHandler<String>> promiseHandler = (HttpRequest req) -> HttpResponse.BodyHandlers.ofString(); public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://http2.golang.org/serverpush")) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofString(), pushPromiseHandler()) .thenApply(HttpResponse::body) .thenAccept((b) -> System.out.println("

Main resource:

" + b)) .join(); System.out.println("

Push promises map size: " + promisesMap.size() + "

"); promisesMap.entrySet().forEach((entry) -> { System.out.println("Request = " + entry.getKey() + ",

Response = " + entry.getValue().join().body()); }); } private static HttpResponse.PushPromiseHandler<String> pushPromiseHandler() { return HttpResponse.PushPromiseHandler.of(promiseHandler, promisesMap); }





The complete source code is available on GitHub.

In both solutions of the preceding solutions, we have used a BodyHandler of the String type via ofString() . This is not very useful if the server pushes binary data as well (for example, images). So, if we are dealing with binary data, we need to switch to BodyHandler of the byte[] type via ofByteArray() . Alternatively, we can send the pushed resources to disk via ofFile() , as shown in the following solution, which is an adapted version of the preceding solution:

private static final ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<Path>>> promisesMap = new ConcurrentHashMap<>(); private static final Function<HttpRequest, HttpResponse.BodyHandler<Path>> promiseHandler = (HttpRequest req) -> HttpResponse.BodyHandlers.ofFile( Paths.get(req.uri().getPath()).getFileName()); public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://http2.golang.org/serverpush")) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofFile( Path.of("index.html")), pushPromiseHandler()) .thenApply(HttpResponse::body) .thenAccept((b) -> System.out.println("

Main resource:

" + b)) .join(); System.out.println("

Push promises map size: " + promisesMap.size() + "

"); promisesMap.entrySet().forEach((entry) -> { System.out.println("Request = " + entry.getKey() + ",

Response = " + entry.getValue().join().body()); }); } private static HttpResponse.PushPromiseHandler<Path> pushPromiseHandler() { return HttpResponse.PushPromiseHandler.of(promiseHandler, promisesMap); }





The preceding code should save the pushed resources in the application classpath, as

shown in the following screenshot:

The complete source code is available on GitHub.

If you enjoyed this article, then I'm sure you will love my book, Java Coding Problems, which has an entire chapter dedicated to HTTP Client API. Check it out!

Further Reading

Java 11: Standardized HTTP Client API

Benefits of REST APIs With HTTP/2