Exploring the new Java 11 HttpClient

Exploring the new Java 11 HttpClient
Photo by Ashwini Chaudhary(Monty) / Unsplash

Java 11 is hot off the press and it comes with a brand new HttpClient class. Say good-bye to fussing with HttpUrlConnections! Let's dive right in by having a look at how we go about creating one and then we'll explore how to make requests.

The HttpClient class uses the builder pattern so that you can have full control over customizing it. If you aren't interested in making any changes the class also provides a static factory method HttpClient.newHttpClient() that will give you a client with some sane defaults.

What options does the builder provide for us?

You probably won't need to use all of these methods on every client you ever build but it's good to know what options we have available.

  • authenticator - If you are going to need to request some credentials from your user when making requests with your client then this method will let you pass in your own custom java.net.Authenticator in order to handle authentication.

  • connectTimeout - This is a pretty standard configuration option that you would expect your http client to offer. It simply sets how long the client should attempt to open a connection before giving up.

  • cookieHandler - If you need to provide your own implementation of Java's java.net.CookieHandler the builder has you covered here.

  • executor - This method will let you supply your own executor which the http client will then use to send any of its async requests.

  • followRedirects - Simply specifies if you want your client to follow any http redirects

  • priority - If your server supports http2 then you can use this to set your default http2 request priority

  • proxy - Allows you to pass your own java.net.ProxySelector. This can be useful if you need to provide users of your application a way to input their own proxy settings.

  • sslContext - If the default SSLContext isn't enough for your https needs then you can provide your own context here.

  • sslParameters - If you need to make sure only certain ciphersuites are acceptable to your client then your custom implementation of java.net.ssl.SSLParameters can be provided here.

  • version - You can use this to specificy the HTTP version you'd like to use for your requests. By default the client will attempt to use HTTP2 and fallback to HTTP 1.1. That is usually a good enough default in my book.

Ok, so we can easily create one of these things with either the provided default factory method or with an easy to use builder object. How do we make a request and get some work done?

We'll need to create HttpRequest objects that we can then send using the HttpClient's send and sendAsync methods. Luckily for us the HttpRequest class also provides a simple to use Builder object that we can use to construct our requests.

Let's look at an example of the simplest GET request we can make.

    var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .GET()
        .build();

    HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

    System.out.println(response.body());

The first thing you'll see in our example is how easy it us to create an HttpRequest object. We just give it a URI and set it's Http verb before calling build. Once we have the request object we just use the send method on the http client.
Since we used the send method we are going to block the current thread until the response comes back. We'll look at sendAsync next to see how you can create non blocking requests.

You'll notice that we also provided a BodyHandler when we made the request.
There are several built in BodyHandlers provided for our convenience. The BodyHandler we used in our example will interpret the response body as a string and ensure that the HttpResponse object we get back from the call returns a string when we call it's body method.

After that you could very easily hand the body to the object serialization/deserialization library of your choice to turn it into a more useful abstraction for your application.
You could also get even fancier and implement your own BodyHandler and BodyPublisher using your object serialization/deserialization library and get HttpResponse objects with body methods that return your application's DTO objects.

So now we can get information from our api but how do we send information using the HttpClient? Sending PUT and POST methods work in a similar fashion.

//PUT
var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .PUT(BodyPublishers.ofString("the string version of my request body"))
        .build();

    HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

    System.out.println(response.body());

//POST
var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .POST(BodyPublishers.ofString("the string version of my request body"))
        .build();

    HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

    System.out.println(response.body());

There is one difference in out PUT and POST examples. We need to provide a body to our request when we call PUT and POST. We can make use of the BodyPublishers.ofString() method to make it so we can simply pass a string in as the body. You are of course free to implement your own BodyPublisher capable of converting your application's java objects into a request body. I'll leave that as an exercise to the reader and stick to using gson and BodyPublishers.ofString() ;)

So far we've seen how to send some pretty bland requests. Most api's I've interacted with in my career have required at the very least some kind of Authorization header so let's look at how we can add headers to our requests.

@Test
  public void simpleHeader() throws IOException, InterruptedException {
    var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .header("Authorization", "Bearer my-secret-key")
        .POST(BodyPublishers.ofString("the string version of my request body"))
        .build();

    HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

    System.out.println(response.body());
  }

The header method provided by our request builder allows us to specify two strings, one for the header name and one for the header value. It's important to understand that this will add the value to the set of values that may already be set for that header on this request.
If we wanted to make sure that the value we were setting was the only value set and to override/remove any previously set values we would use setHeader like this:

var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .setHeader("Authorization", "Bearer only-my-secret-key-please")
        .POST(BodyPublishers.ofString("the string version of my request body"))
        .build();

    HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

    System.out.println(response.body());

There's one other convenience method offered to us by the builder that lets us pass in a variable number of strings as long as we follow the pattern of alternating header name and header value.


var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .headers("Authorization", "Bearer only-my-secret-key-please", "Content-Type", "application/json", "Accept", "application/json")
        .POST(BodyPublishers.ofString("the string version of my request body"))
        .build();

    HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

    System.out.println(response.body());

So far all of our examples have made blocking requests using the HttpClient's send method. What if we don't want our http client requests to block our current execution thread? We can use sendAsync for those scenarios.

 var httpClient = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .setHeader("Authorization", "Bearer only-my-secret-key-please")
        .POST(BodyPublishers.ofString("the string version of my request body"))
        .build();

    var completableFuture = httpClient.sendAsync(request, BodyHandlers.ofString());
    completableFuture.thenAccept(response -> System.out.println(response.body()));

When we use sendAsync we will get back a CompletableFuture which we can then do a number of things with. We could probably fill another blog post or two with all ways we can use CompletableFutures. We'll stick to a simple example for this post though to keep our focus on the HttpClient.

In this first example we use the thenAccept method of the CompletableFuture to simple print out the request's body as soon as it has completed. Let's look at another example now. One of the most common use cases we might have is a scenario where we need to hit two different endpoints and use data from both endpoints to decide on what happens next. Without something like CompletableFutures this is the kind of situation that will quickly lead you down the road to callback hell, but with CompletableFuture we can very easily combine our requests and avoid any headaches.


var httpClient = HttpClient.newHttpClient();

    HttpRequest firstRequest = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someEndPoint"))
        .GET()
        .build();

    HttpRequest secondRequest = HttpRequest.newBuilder(URI.create("https://api.myapp.com/someOtherEndPoint"))
        .GET()
        .build();

    var firstFuture = httpClient.sendAsync(firstRequest, BodyHandlers.ofString());
    var secondFuture = httpClient.sendAsync(secondRequest, BodyHandlers.ofString());

    BiFunction<HttpResponse<String>, HttpResponse<String>, String> combinerFunction =
        (firstResponse, secondResponse) -> firstResponse.body() + " : " + secondResponse.body();

    firstFuture
        .thenCombine(secondFuture, combinerFunction)
        .thenAccept(System.out::println);


In this example we make two different requests to two different endpoints and then use the CompletableFuture api to combine the response body of those two requests once they have both completed and then finally we print the combined string to the console.

Ok, so at this point we've covered enough to see how easy it is to do your most common http request chores with the new http client, but it's 2018 and we have websocket needs. Can the new http client help us out with those?
As a matter of fact it can. Let's take a look at how we can setup a websocket listener with our http client.

var httpClient = HttpClient.newHttpClient();

    httpClient.newWebSocketBuilder().buildAsync(
        URI.create("https://api.myapp.com/someWebsocketEndPoint"), new Listener() {
          @Override
          public void onOpen(WebSocket webSocket) {
            System.out.println("Web socket open for business");
            webSocket.sendText("We've seen our onOpen callback", true);
          }

          @Override
          public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
            return null;
          }

          @Override
          public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
            return null;
          }

          @Override
          public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) {
            return null;
          }

          @Override
          public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
            return null;
          }

          @Override
          public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
            return null;
          }

          @Override
          public void onError(WebSocket webSocket, Throwable error) {
            System.out.println("Our websocket produced an error :(");
          }
        });

As you can see wiring up a websocket is as simple as calling a method on the http client and passing it your websocket endpoint along with a listener. Your production listeners are gonna wanna do more than just return null.

That's it for this post. I've provided a small repository here that includes all of the above examples.