HTTP/2 Server Push Via Java 11 HTTP Client API
Do you remember HttpUrlConnection? Well, JDK 11 has reinvented this server push using the HTTP Client API. Check it out!
Join the DZone community and get the full member experience.
Join For FreeDo 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(
"\nPushed resource body:\n " + b));
asyncPushRequests.add(pushcf);
System.out.println("\nJust got promise push number: " +
asyncPushRequests.size());
System.out.println("\nInitial 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("\nMain resource:\n" + b))
.join();
asyncPushRequests.forEach(CompletableFuture::join);
System.out.println("\nFetched 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("\nMain resource:\n" + b))
.join();
System.out.println("\nPush promises map size: " +
promisesMap.size() + "\n");
promisesMap.entrySet().forEach((entry) -> {
System.out.println("Request = " + entry.getKey() +
", \nResponse = " + 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 toBodyHandler
of thebyte[]
type via ofByteArray()
. Alternatively, we can send the pushed resources to disk viaofFile()
, 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("\nMain resource:\n" + b))
.join();
System.out.println("\nPush promises map size: " +
promisesMap.size() + "\n");
promisesMap.entrySet().forEach((entry) -> {
System.out.println("Request = " + entry.getKey() +
", \nResponse = " + 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
Opinions expressed by DZone contributors are their own.
Comments