1. Overview
In this tutorial, we’ll explore Java 11’s standardization of HTTP client API that implements HTTP/2 and Web Socket.
It aims to replace the legacy HttpUrlConnection class that has been present in the JDK since the very early years of Java.
Until very recently, Java provided only the HttpURLConnection API, which is low-level and isn’t known for being feature-rich and user-friendly.
Therefore, some widely used third-party libraries were commonly used, such as Apache HttpClient, Jetty and Spring’s RestTemplate.
2. Background
The change was implemented as a part of JEP 321.
2.1. Major Changes as Part of JEP 321
- The incubated HTTP API from Java 9 is now officially incorporated into the Java SE API. The new HTTP APIs can be found in java.net.HTTP.*
- The newer version of the HTTP protocol is designed to improve the overall performance of sending requests by a client and receiving responses from the server. This is achieved by introducing a number of changes such as stream multiplexing, header compression and push promises.
- As of Java 11, the API is now fully asynchronous (the previous HTTP/1.1 implementation was blocking). Asynchronous calls are implemented using CompletableFuture.The CompletableFuture implementation takes care of applying each stage once the previous one has finished, so this whole flow is asynchronous.
- The new HTTP client API provides a standard way to perform HTTP network operations with support for modern Web features such as HTTP/2, without the need to add third-party dependencies.
- The new APIs provide native support for HTTP 1.1/2 WebSocket. The core classes and interface providing the core functionality include:
- The HttpClient class, java.net.http.HttpClient
- The HttpRequest class, java.net.http.HttpRequest
- The HttpResponse
interface, java.net.http.HttpResponse - The WebSocket interface, java.net.http.WebSocket
2.2. Problems With the Pre-Java 11 HTTP Client
The existing HttpURLConnection API and its implementation had numerous problems:
- URLConnection API was designed with multiple protocols that are now no longer functioning (FTP, gopher, etc.).
- The API predates HTTP/1.1 and is too abstract.
- It works in blocking mode only (i.e., one thread per request/response).
- It is very hard to maintain.
3. HTTP Client API Overview
Unlike HttpURLConnection, HTTP Client provides synchronous and asynchronous request mechanisms.
The API consists of three core classes:
- HttpRequest represents the request to be sent via the HttpClient.
- HttpClient behaves as a container for configuration information common to multiple requests.
- HttpResponse represents the result of an HttpRequest call.
We’ll examine each of them in more details in the following sections. First, let’s focus on a request.
4. HttpRequest
HttpRequest is an object that represents the request we want to send. New instances can be created using HttpRequest.Builder.
We can get it by calling HttpRequest.newBuilder(). Builder class provides a bunch of methods that we can use to configure our request.
We’ll cover the most important ones.
Note: In JDK 16, there is a new HttpRequest.newBuilder(HttpRequest request, BiPredicate<String,String> filter) method, which creates a Builder whose initial state is copied from an existing HttpRequest.
This builder can be used to build an HttpRequest, equivalent to the original, while allowing amendment of the request state prior to construction, for example, removing headers:
HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Foo-Bar"))
4.1. Setting URI
The first thing we have to do when creating a request is to provide the URL.
We can do that in two ways — using the constructor for Builder with URI parameter or calling method uri(URI) on the Builder instance:
HttpRequest.newBuilder(new URI("https://postman-echo.com/get"))
HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
The last thing we have to configure to create a basic request is an HTTP method.
4.2. Specifying the HTTP Method
We can define the HTTP method that our request will use by calling one of the methods from Builder:
- GET()
- POST(BodyPublisher body)
- PUT(BodyPublisher body)
- DELETE()
We’ll cover BodyPublisher in detail, later.
Now let’s just create a very simple GET request example:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.GET()
.build();
This request has all parameters required by HttpClient.
However, we sometimes need to add additional parameters to our request. Here are some important ones:
- The version of the HTTP protocol
- Headers
- A timeout
4.3. Setting HTTP Protocol Version
The API fully leverages the HTTP/2 protocol and uses it by default, but we can define which version of the protocol we want to use:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
Important to mention here is that the client will fall back to, e.g., HTTP/1.1 if HTTP/2 isn’t supported.
4.4. Setting Headers
In case we want to add additional headers to our request, we can use the provided builder methods.
We can do that by either passing all headers as key-value pairs to the headers() method or by using header() method for the single key-value header:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.headers("key1", "value1", "key2", "value2")
.GET()
.build();
HttpRequest request2 = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.header("key1", "value1")
.header("key2", "value2")
.GET()
.build();
The last useful method we can use to customize our request is a timeout().
4.5. Setting a Timeout
Let’s now define the amount of time we want to wait for a response.
If the set time expires, a HttpTimeoutException will be thrown. The default timeout is set to infinity.
The timeout can be set with the Duration object by calling method timeout() on the builder instance:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.timeout(Duration.of(10, SECONDS))
.GET()
.build();
5. Setting a Request Body
We can add a body to a request by using the request builder methods: POST(BodyPublisher body), PUT(BodyPublisher body) and DELETE().
The new API provides a number of BodyPublisher implementations out-of-the-box that simplify passing the request body:
- StringProcessor – reads body from a String, created with HttpRequest.BodyPublishers.ofString
- InputStreamProcessor – reads body from an InputStream, created with HttpRequest.BodyPublishers.ofInputStream
- ByteArrayProcessor – reads body from a byte array, created with HttpRequest.BodyPublishers.ofByteArray
- FileProcessor – reads body from a file at the given path, created with HttpRequest.BodyPublishers.ofFile
In case we don’t need a body, we can simply pass in an *HttpRequest.BodyPublishers.*noBody():
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
Note: In JDK 16, there’s a new HttpRequest.BodyPublishers.concat(BodyPublisher…) method that helps us building a request body from the concatenation of the request bodies published by a sequence of publishers. The request body published by a concatenation publisher is logically equivalent to the request body that would have been published by concatenating all the bytes of each publisher in sequence.
5.1. StringBodyPublisher
Setting a request body with any BodyPublishers implementation is very simple and intuitive.
For example, if we want to pass a simple String as a body, we can use StringBodyPublishers.
As we already mentioned, this object can be created with a factory method ofString() — it takes just a String object as an argument and creates a body from it:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
.build();
5.2. InputStreamBodyPublisher
To do that, the InputStream has to be passed as a Supplier (to make its creation lazy), so it’s a little bit different than StringBodyPublishers.
However, this is also quite straightforward:
byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers
.ofInputStream(() -> new ByteArrayInputStream(sampleData)))
.build();
Notice how we used a simple ByteArrayInputStream here. Of course, that can be any InputStream implementation.
5.3. ByteArrayProcessor
We can also use ByteArrayProcessor and pass an array of bytes as the parameter:
byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofByteArray(sampleData))
.build();
5.4. FileProcessor
To work with a File, we can make use of the provided FileProcessor.
Its factory method takes a path to the file as a parameter and creates a body from the content:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.fromFile(
Paths.get("src/test/resources/sample.txt")))
.build();
We’ve covered how to create HttpRequest and how to set additional parameters in it.
Now it’s time to take a deeper look at HttpClient class, which is responsible for sending requests and receiving responses.
6. HttpClient
All requests are sent using HttpClient, which can be instantiated using the HttpClient.newBuilder() method or by calling HttpClient.newHttpClient().
It provides a lot of useful and self-describing methods we can use to handle our request/response.
Let’s cover some of these here.
6.1. Handling Response Body
Similar to the fluent methods for creating publishers, there are methods dedicated to creating handlers for common body types:
BodyHandlers.ofByteArray
BodyHandlers.ofString
BodyHandlers.ofFile
BodyHandlers.discarding
BodyHandlers.replacing
BodyHandlers.ofLines
BodyHandlers.fromLineSubscriber
Pay attention to the usage of the new BodyHandlers factory class.
Before Java 11, we had to do something like this:
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandler.asString());
And we can now simplify it:
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
6.2. Setting a Proxy
We can define a proxy for the connection by just calling proxy() method on a Builder instance:
HttpResponse<String> response = HttpClient
.newBuilder()
.proxy(ProxySelector.getDefault())
.build()
.send(request, BodyHandlers.ofString());
In our example, we used the default system proxy.
6.3. Setting the Redirect Policy
Sometimes the page we want to access has moved to a different address.
In that case, we’ll receive HTTP status code 3xx, usually with the information about new URI. HttpClient can redirect the request to the new URI automatically if we set the appropriate redirect policy.
We can do it with the followRedirects() method on Builder:
HttpResponse<String> response = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()
.send(request, BodyHandlers.ofString());
All policies are defined and described in enum HttpClient.Redirect.
6.4. Setting Authenticator for a Connection
An Authenticator is an object that negotiates credentials (HTTP authentication) for a connection.
It provides different authentication schemes (such as basic or digest authentication).
In most cases, authentication requires username and password to connect to a server.
We can use PasswordAuthentication class, which is just a holder of these values:
HttpResponse<String> response = HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
"username",
"password".toCharArray());
}
}).build()
.send(request, BodyHandlers.ofString());
Here we passed the username and password values as a plaintext. Of course, this would have to be different in a production scenario.
Note that not every request should use the same username and password. The Authenticator class provides a number of getXXX (e.g., getRequestingSite()) methods that can be used to find out what values should be provided.
Now we’re going to explore one of the most useful features of new HttpClient — asynchronous calls to the server.
6.5. Send Requests – Sync vs Async
New HttpClient provides two possibilities for sending a request to a server:
- send(…) – synchronously (blocks until the response comes)
- sendAsync(…) – asynchronously (doesn’t wait for the response, non-blocking)
Up until now, the send(...) method naturally waits for a response:
HttpResponse<String> response = HttpClient.newBuilder()
.build()
.send(request, BodyHandlers.ofString());
This call returns an HttpResponse object, and we’re sure that the next instruction from our application flow will be run only when the response is already here.
However, it has a lot of drawbacks especially when we are processing large amounts of data.
So, now we can use sendAsync(...) method — which returns CompletableFeature
CompletableFuture<HttpResponse<String>> response = HttpClient.newBuilder()
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
The new API can also deal with multiple responses, and stream the request and response bodies:
List<URI> targets = Arrays.asList(
new URI("https://postman-echo.com/get?foo1=bar1"),
new URI("https://postman-echo.com/get?foo2=bar2"));
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = targets.stream()
.map(target -> client
.sendAsync(
HttpRequest.newBuilder(target).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(response -> response.body()))
.collect(Collectors.toList());
6.6. Setting Executor for Asynchronous Calls
We can also define an Executor that provides threads to be used by asynchronous calls.
This way we can, for example, limit the number of threads used for processing requests:
ExecutorService executorService = Executors.newFixedThreadPool(2);
CompletableFuture<HttpResponse<String>> response1 = HttpClient.newBuilder()
.executor(executorService)
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
CompletableFuture<HttpResponse<String>> response2 = HttpClient.newBuilder()
.executor(executorService)
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
By default, the HttpClient uses executor java.util.concurrent.Executors.newCachedThreadPool().
6.7. Defining a CookieHandler
With new API and builder, it’s straightforward to set a CookieHandler for our connection. We can use builder method cookieHandler(CookieHandler cookieHandler) to define client-specific CookieHandler.
Let’s define *CookieManager (*a concrete implementation of CookieHandler that separates the storage of cookies from the policy surrounding accepting and rejecting cookies) that doesn’t allow to accept cookies at all:
HttpClient.newBuilder()
.cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
.build();
In case our CookieManager allows cookies to be stored, we can access them by checking CookieHandler from our HttpClient:
((CookieManager) httpClient.cookieHandler().get()).getCookieStore()
Now let’s focus on the last class from Http API — the HttpResponse.
7. HttpResponse Object
The HttpResponse class represents the response from the server. It provides a number of useful methods, but these are the two most important:
- statusCode() returns status code (type int) for a response (HttpURLConnection class contains possible values).
- body() returns a body for a response (return type depends on the response BodyHandler parameter passed to the send() method).
The response object has other useful methods that we’ll cover such as uri(), headers(), trailers() and version().
7.1. URI of Response Object
The method uri() on the response object returns the URI from which we received the response.
Sometimes it can be different than URI in the request object because a redirection may occur:
assertThat(request.uri()
.toString(), equalTo("http://stackoverflow.com"));
assertThat(response.uri()
.toString(), equalTo("https://stackoverflow.com/"));
7.2. Headers from Response
We can obtain headers from the response by calling method headers() on a response object:
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
HttpHeaders responseHeaders = response.headers();
It returns HttpHeaders object, which represents a read-only view of HTTP Headers.
It has some useful methods that simplify searching for headers value.
7.3. Version of the Response
The method version() defines which version of HTTP protocol was used to talk with a server.
Remember that even if we define that we want to use HTTP/2, the server can answer via HTTP/1.1.
The version in which the server answered is specified in the response:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.version(), equalTo(HttpClient.Version.HTTP_1_1));
8. Handling Push Promises in HTTP/2
New HttpClient supports push promises through PushPromiseHandler interface*.*
It allows the server to “push” content to the client additional resources while requesting the primary resource, saving more roundtrip and as a result improves performance in page rendering.
It is really the multiplexing feature of HTTP/2 that allows us to forget about resource bundling. For each resource, the server sends a special request, known as a push promise to the client.
Push promises received, if any, are handled by the given PushPromiseHandler. A null valued PushPromiseHandler rejects any push promises.
The HttpClient has an overloaded sendAsync method that allows us to handle such promises, as shown below.
Let’s first create a PushPromiseHandler:
private static PushPromiseHandler<String> pushPromiseHandler() {
return (HttpRequest initiatingRequest,
HttpRequest pushPromiseRequest,
Function<HttpResponse.BodyHandler<String>,
CompletableFuture<HttpResponse<String>>> acceptor) -> {
acceptor.apply(BodyHandlers.ofString())
.thenAccept(resp -> {
System.out.println(" Pushed response: " + resp.uri() + ", headers: " + resp.headers());
});
System.out.println("Promise request: " + pushPromiseRequest.uri());
System.out.println("Promise request: " + pushPromiseRequest.headers());
};
}
Next, let’s use sendAsync method to handle this push promise:
httpClient.sendAsync(pageRequest, BodyHandlers.ofString(), pushPromiseHandler())
.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();
9. Conclusion
In this article, we explored the Java 11 HttpClient API that standardized the incubating HttpClient introduced in Java 9 with more powerful changes.
The complete code used can be found over on GitHub.
In the examples, we’ve used sample REST endpoints provided by https://postman-echo.com.