HTTP/2 Server Push and Handling Push Promises

HTTP/2 Server Push is one of the performance features included in version 2 of the HTTP protocol that allows the Web server to "push" content to the client ahead of time (before the client requests it) as long as all the URLs are delivered over the same host-name and protocol. It's based on the client's good faith of accepting a promise sent by a server for page assets (images, js and css files, etc) are likely to be needed by the client.

As example, when I open https://http2.golang.org/serverpush in my browser (chrome), I get following entries in my Developer tools -> Network panel logs

HTTP/2 Server Push

HTTP/2 Server Push

You can see from log above the initator of: style.css, jquery.min.js, playground.js, and godocs.js requests is "Push / serverpush". In this case, instead of the client (browser) having to request every page asset, the server can guess which resources are likely to be needed by the client and push them to the client. For each resource, the server sends a special request, known as a push promise to the client.

The HttpClient has an overloaded sendAsync method that allows us to handle such promises, as shown in the below example:

JEP321Http2ServerPush.java
package com.dariawan.jdk11;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

public class JEP321Http2ServerPush {

    static ExecutorService executor = Executors.newFixedThreadPool(6, (Runnable r) -> {
        return new Thread(r);
    });

    public static void main(String[] args) throws Exception {
        System.out.println("Running HTTP/2 Server Push example...");
        try {
            HttpClient httpClient = HttpClient.newBuilder()
                    .version(Version.HTTP_2)
                    .build();

            HttpRequest pageRequest = HttpRequest.newBuilder()
                    .uri(URI.create("https://http2.golang.org/serverpush"))
                    .build();

            AtomicInteger atomicInt = new AtomicInteger(1);
            // Interface HttpResponse.PushPromiseHandler<T>
            // void applyPushPromise​(HttpRequest initiatingRequest, HttpRequest pushPromiseRequest, Function<HttpResponse.BodyHandler<T>,​CompletableFuture<HttpResponse<T>>> acceptor)
            httpClient.sendAsync(pageRequest, BodyHandlers.ofString(), 
                    (HttpRequest initiatingRequest, HttpRequest pushPromiseRequest, Function<HttpResponse.BodyHandler<String>, CompletableFuture<HttpResponse<String>>> acceptor) -> {
                acceptor.apply(BodyHandlers.ofString()).thenAccept(resp -> {
                    System.out.println("[" + atomicInt.getAndIncrement() + "] Pushed response: " + resp.uri() + ", headers: " + resp.headers());
                });
                System.out.println("Promise request: " + pushPromiseRequest.uri());
                System.out.println("Promise request: " + pushPromiseRequest.headers());                
            }).thenAccept(pageResponse -> {
                System.out.println("Page response status code: " + pageResponse.statusCode());
                System.out.println("Page response headers: " + pageResponse.headers());
                String responseBody = pageResponse.body();
                System.out.println(responseBody);
            }).join();
            
            Thread.sleep(1000);  // waiting for full response
        } finally {
            executor.shutdown();
        }
    }
}
                    

And when we run, the result is:

Running HTTP/2 Server Push example... Promise request: https://http2.golang.org/serverpush/static/style.css?1549212157991835046 Promise request: java.net.http.HttpHeaders@cd4f7b20 { {:authority=[http2.golang.org], :method=[GET], :path=[/serverpush/static/style.css?1549212157991835046], :scheme=[https]} } Promise request: https://http2.golang.org/serverpush/static/jquery.min.js?1549212157991835046 Promise request: java.net.http.HttpHeaders@67f53a71 { {:authority=[http2.golang.org], :method=[GET], :path=[/serverpush/static/jquery.min.js?1549212157991835046], :scheme=[https]} } Promise request: https://http2.golang.org/serverpush/static/godocs.js?1549212157991835046 Promise request: java.net.http.HttpHeaders@422a9fb2 { {:authority=[http2.golang.org], :method=[GET], :path=[/serverpush/static/godocs.js?1549212157991835046], :scheme=[https]} } Promise request: https://http2.golang.org/serverpush/static/playground.js?1549212157991835046 Promise request: java.net.http.HttpHeaders@167a8d0a { {:authority=[http2.golang.org], :method=[GET], :path=[/serverpush/static/playground.js?1549212157991835046], :scheme=[https]} } [1] Pushed response: https://http2.golang.org/serverpush/static/playground.js?1549212157991835046, headers: java.net.http.HttpHeaders@13d55ec4 { {:status=[200], accept-ranges=[bytes], content-length=[13487], content-type=[application/javascript], date=[Sun, 03 Feb 2019 16:42:38 GMT], last-modified=[Sat, 02 Feb 2019 00:30:18 GMT]} } [2] Pushed response: https://http2.golang.org/serverpush/static/godocs.js?1549212157991835046, headers: java.net.http.HttpHeaders@13db7f3c { {:status=[200], accept-ranges=[bytes], content-length=[17807], content-type=[application/javascript], date=[Sun, 03 Feb 2019 16:42:38 GMT], last-modified=[Sat, 02 Feb 2019 00:30:18 GMT]} } Page response status code: 200 Page response headers: java.net.http.HttpHeaders@50d125d4 { {:status=[200], content-type=[text/html; charset=utf-8], date=[Sun, 03 Feb 2019 16:42:38 GMT]} } <!DOCTYPE html> <html> ... /* HTML BODY IS REMOVED FROM THIS SNAPSHOT */ ... </html> [3] Pushed response: https://http2.golang.org/serverpush/static/style.css?1549212157991835046, headers: java.net.http.HttpHeaders@e301fcd9 { {:status=[200], accept-ranges=[bytes], content-length=[13261], content-type=[text/css; charset=utf-8], date=[Sun, 03 Feb 2019 16:42:38 GMT], last-modified=[Sat, 02 Feb 2019 00:30:18 GMT]} } [4] Pushed response: https://http2.golang.org/serverpush/static/jquery.min.js?1549212157991835046, headers: java.net.http.HttpHeaders@1464135b { {:status=[200], accept-ranges=[bytes], content-length=[93435], content-type=[application/javascript], date=[Sun, 03 Feb 2019 16:42:38 GMT], last-modified=[Sat, 02 Feb 2019 00:30:18 GMT]} }

We get performance boost because this remove a round-trip for requests explicitly made by the client. Instead the resources are pushed by the server along with the initial request. In theory, this means the faster loading of a page.