In this blog post, we will look at the HTTP client library introduced in Java 11 (September 2018). It's one of the more significant new features we've got with Java 11. To be exact, the library was already part of Java 9 but only as an incubation module. We may see more of this pattern in the future because of Java's shorter six-month release cycles. It's a way for the Java platform developers to release new features early, collect feedback from the developer community, and, when ready, release it officially, as they did with the HTTP client library.
Up to Java version 11, the only built-in way to work with HTTP is URLConnection (available since Java 1.0). However, it's not the most straightforward API to work with and does not support the newer HTTP/2 protocol. Because of that, most projects added an external HTTP client library, like Apache HTTP Client and OkHttp to their projects. I think there is still room for these libraries on Java 11 because, as you will see later, the new Java 11 HTTP client misses a few convenient functions like URI builder, multipart form data, form data, and compression support. Especially the missing compression support surprised me a little bit coming from OkHttp, where the library transparently handles this functionality. However, these missing features are no deal-breakers and can be implemented on top of the new HTTP client library, which speaks for the flexibility and configurability of the library. You find examples of these features in the demo applications below.
In the following article, I show you multiple examples of the library. Most examples speak with a local Spring Boot server.
You find the code for this application on GitHub:
https://github.com/ralscha/blog2019/tree/master/java11httpclient/server
All examples use HTTP/2 and TLS, but you can disable it in application.properties
. Only the HTTP/2 Server Push demo requires HTTP/2 and TLS. All the other examples also work with HTTP/1.1 and cleartext HTTP.
I utilized mkcert
to install a private CA and to create the TLS certificate (see my previous blog post).
Make sure that you set the environment variable JAVA_HOME
correctly before running mkcert --install
, so mkcert
can add the root CA to the Java installation. This is important for the client part of the demos because they trust this private root CA.
Overview ¶
The Java 11 HTTP client is part of the Java SE platform and comprises the following classes and interfaces that all reside in the java.net.http
package (module: java.net.http
).
This is not a complete list of all available classes and interfaces. Visit the JavaDoc to see a complete overview: https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html
java.net.HttpClient ¶
You first have to create an HttpClient with a builder-style API to send requests. You can configure per-client settings when building the client.
Here is an example that shows all available settings.
var client = HttpClient.newBuilder()
.authenticator(Authenticator.getDefault())
.connectTimeout(Duration.ofSeconds(30))
.cookieHandler(CookieHandler.getDefault())
.executor(Executors.newFixedThreadPool(2))
.followRedirects(Redirect.NEVER)
.priority(1) //HTTP/2 priority
.proxy(ProxySelector.getDefault())
.sslContext(SSLContext.getDefault())
.version(Version.HTTP_2)
.sslParameters(new SSLParameters())
.build();
Once created, an HttpClient
instance is immutable, thus automatically thread-safe, and you can send multiple requests with it.
By default, the client tries to open an HTTP/2 connection. If the server answers with HTTP/1.1, the client automatically falls back to this version.
If you know in advance that the server only speaks HTTP/1.1, you may create the client with version(Version.HTTP_1_1)
.
connectTimeout()
determines how long the client waits until a connection can be established. If the connection can't be established, the client throws a HttpConnectTimeoutException
exception.
executor()
sets the executor to be used for asynchronous and dependent tasks. A default executor is created for each newly built HttpClient
if you don't specify an executor.
The default executor uses a thread pool.
See the examples below for more information about followRedirects()
, authenticator()
and cookieHandler()
.
If you are okay with the default settings, you can build the client with newHttpClient()
.
client = HttpClient.newHttpClient();
// equivalent
client = HttpClient.newBuilder().build();
The default settings include:
- prefer HTTP/2
- no connection timeout
- redirection policy of NEVER
- no cookie handler
- no authenticator
- default thread pool executor
- default proxy selector
- default SSL context
java.net.HttpRequest ¶
Similar to clients, you build HttpRequest
instances with a builder. You must set the URI request method and specify the body, timeout, and headers optionally.
var request = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8443/headers"))
.timeout(Duration.ofMinutes(2))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString("the body"))
.build();
HttpRequest
instances are immutable and can be sent multiple times.
The request URI can be specified with uri()
or as argument of newBuilder()
.
There is no difference in functionality.
var request1 = HttpRequest.newBuilder(URI.create("https://localhost:8443/headers"))
.build();
var request2 = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8443/headers"))
.build();
timeout()
sets a timeout only for this request. If the client does not receive a response in the specified amount of time, he throws an HttpTimeoutException
exception, and sendAsync()
completes exceptionally with this exception. If you don't set a timeout, the client waits forever.
The client supports all HTTP methods, but the request builder only contains these predefined methods: GET()
, POST()
, DELETE()
, and PUT()
.
To create a request with a different HTTP method, you need to call method()
.
Example of a HEAD request:
var request = HttpRequest.newBuilder(URI.create("https://localhost:8443/headers"))
.method("HEAD", BodyPublishers.noBody())
.build();
The special BodyPublishers.noBody()
can be used where no request body is required.
You may copy the builder if you need to create multiple similar requests
var client = HttpClient.newHttpClient();
var builder = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/headers"));
var request1 = builder.copy().setHeader("X-Counter", "1").build();
var request2 = builder.copy().setHeader("X-Counter", "2").build();
java.net.HttpResponse ¶
HttpResponse
is an interface and not created directly. Implementations of this interface are returned by the client when sending a request.
A few of the provided methods in the response interface.
Return type | Method | Description |
---|---|---|
T | body() |
returns the response body |
HttpHeaders | headers() |
returns the response headers |
HttpRequest | request() |
returns the HttpRequest corresponding to this response |
int | statusCode() |
returns the HTTP status code |
URI | uri() |
returns the URI that the response was received from |
HttpClient.Version | version() |
returns the HTTP protocol |
See the JavaDoc for a complete overview: https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpResponse.html
The request and response bodies are exposed as reactive streams (asynchronous data streams with
non-blocking back pressure). They use the new reactive stream support that has been introduced in Java 9 (Flow
)
HttpRequest.BodyPublisher ¶
Subtype of Flow.Publisher
A BodyPublisher is used when you send a request with a request body. The BodyPublisher converts objects into a flow of byte buffers suitable for sending as a body.
HttpRequest.BodyPublishers ¶
Implementations of BodyPublisher that implement various useful publishers, such as publishing the request body from a String, or from a file.
Here are a few examples
BodyPublishers::ofString
BodyPublishers::ofFile
BodyPublishers::ofByteArray
BodyPublishers::ofInputStream
HttpResponse.BodyHandler ¶
BodyHandlers are responsible for converting the bytes from a response into higher-level Java types, like String or Path. This interface allows inspection of the status code and headers before the actual response body is received
HttpResponse.BodyHandlers ¶
Factory class provides BodyHandler implementation for handling common response body types such as files, Strings, and bytes. These implementations do not examine the status code. They typically return a
HttpResponse.BodySubscribers
with the same name
A few examples
BodyHandlers::ofByteArray
BodyHandlers::ofFile
BodyHandlers::ofString
BodyHandlers::ofInputStream
WebSocket ¶
The Java 11 HTTP client supports HTTP and includes a WebSocket client. The last demo application in this blog post shows you an example with WebSocket.
Sending requests ¶
Requests can be sent either synchronously or asynchronously.
Synchronously ¶
send()
blocks the calling thread until the response is available.
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
Asynchronously ¶
sendAsync()
immediately returns with a CompletableFuture that completes with a HttpResponse when it becomes available.
CompletableFuture<Void> future = client.sendAsync(request, BodyHandlers.ofString())
.thenApply(response -> {
System.out.println(response.statusCode());
return response;
})
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);
Asynchronous requests are handled by an executor service that the HTTP client automatically manages, or use a custom executor when configured with executor()
In the following examples, I switch between the two styles. Both styles provide the same functionality. It depends on the overall architecture of your application how you want to send the requests. You may, without any problem, use both styles in one application.
GET ¶
GET is maybe the most widely used HTTP method, which is reflected in the HttpClient design.
You may omit GET()
when you build a request. The HttpClient assumes it's a GET request by default.
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/helloworld"))
.timeout(Duration.ofSeconds(15))
.build();
try {
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
printResponse(response);
}
catch (IOException | InterruptedException e) {
e.printStackTrace();
}
BodyHandlers.ofString()
is a factory creating a body handler that handles the bytes in the response and converts them to a String. Thus the generic type of HttpResponse
in this example is String.
The same request send asynchronously.
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/helloworld"))
.build();
CompletableFuture<HttpResponse<String>> future = client.sendAsync(request,
BodyHandlers.ofString());
return future.thenApply(response -> {
printResponse(response);
return response;
}).thenApply(HttpResponse::body)
.exceptionally(e -> "Error: " + e.getMessage())
.thenAccept(System.out::println);
}
With the asynchronous API, an application can send multiple requests concurrently.
var client = HttpClient.newHttpClient();
List<HttpRequest> requests = paths.stream()
.map(path -> "https://localhost:8443" + path)
.map(URI::create)
.map(uri -> HttpRequest.newBuilder(uri).build())
.collect(Collectors.toList());
CompletableFuture<?>[] responses = requests.stream()
.map(request -> client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.exceptionally(e -> "Error: " + e.getMessage())
.thenAccept(System.out::println))
.toArray(CompletableFuture<?>[]::new);
JSON ¶
There is no special support for JSON built-in. Instead, it is handled like any other String message.
Because there is no JSON parser built into the Java platform, I added Yasson to this project.
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>1.0.6</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.6</version>
</dependency>
Eclipse Yasson is an official reference implementation of JSON Binding (JSR-367).
An application can handle a JSON response in different ways. Either convert the response into a String and then use the JSON parser to convert it into an object or create a custom BodyHandler that does the conversion. In this example, you see the latter approach in action.
BodyHandler is an interface and you need to implement the apply()
method. In this example, the JSON parser (jsonb
)
takes the bytes from the response and converts them into the target type.
public class JsonBodyHandler<T> implements HttpResponse.BodyHandler<T> {
private final Jsonb jsonb;
private final Class<T> type;
public static <T> JsonBodyHandler<T> jsonBodyHandler(final Class<T> type) {
return jsonBodyHandler(JsonbBuilder.create(), type);
}
public static <T> JsonBodyHandler<T> jsonBodyHandler(final Jsonb jsonb,
final Class<T> type) {
return new JsonBodyHandler<>(jsonb, type);
}
private JsonBodyHandler(Jsonb jsonb, Class<T> type) {
this.jsonb = jsonb;
this.type = type;
}
@Override
public HttpResponse.BodySubscriber<T> apply(
final HttpResponse.ResponseInfo responseInfo) {
return BodySubscribers.mapping(BodySubscribers.ofByteArray(),
byteArray -> this.jsonb.fromJson(new ByteArrayInputStream(byteArray), this.type));
}
}
You can now conveniently use this BodyHandler to convert a JSON response into a POJO in the main application.
Jsonb jsonb = JsonbBuilder.create();
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://localhost:8443/user"))
.build();
HttpResponse<User> response = client.send(request,
JsonBodyHandler.jsonBodyHandler(jsonb, User.class));
POST ¶
In this section, we will look at some examples with POST. Unlike GET, a POST always requires a body. Therefore you have to pass an instance of BodyPublisher to the POST()
method. If, for some reason, you don't want to send any body, you can call the special BodyPublishers.noBody
factory method.
Text ¶
This example sends the String this is a text
to the server. BodyPublishers.ofString
creates a BodyPublisher that takes a String and converts it into bytes for the request body. This BodyPublisher converts the given String with the UTF-8 character set. There is a second ofString()
method available that
takes the character set as the second argument and uses that for the conversion.
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString("this is a text"))
.uri(URI.create("https://localhost:8443/uppercase"))
.header("Content-Type", "text/plain")
.build();
return client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.exceptionally(e -> "Error: " + e.getMessage())
.thenAccept(System.out::println);
JSON ¶
Like for fetching JSON, there is no special built-in support for posting JSON. Instead, you convert an object to a JSON string and
treat it like any String POST request. Again, make sure that you specify the correct Content-Type
header if the back end depends on this information to work correctly.
Jsonb jsonb = JsonbBuilder.create();
var client = HttpClient.newHttpClient();
User user = new User(2, "Mr. Client");
var request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(jsonb.toJson(user)))
.uri(URI.create("https://localhost:8443/saveUser"))
.header("Content-Type", "application/json")
.build();
HttpResponse<Void> response = client.send(request, BodyHandlers.discarding());
The /saveUser
endpoint does not return anything. Thus I use a special BodyHandler that discards the body.
Formdata (x-www-form-urlencoded) ¶
There is no built-in support to send a POST request with x-www-form-urlencoded
, but it's not that complicated to implement it.
When you send an x-www-form-urlencoded
POST request, the keys and values are encoded in key-value tuples separated by &
with =
between the key and the value.
Non-alphanumeric characters in keys and values must be properly encoded.
A simple utility method takes a Map of key/value pairs and converts them into the proper form for an x-www-form-urlencoded
POST request.
public static BodyPublisher ofFormData(Map<Object, Object> data) {
var builder = new StringBuilder();
for (Map.Entry<Object, Object> entry : data.entrySet()) {
if (builder.length() > 0) {
builder.append("&");
}
builder
.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8));
builder.append("=");
builder
.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8));
}
return BodyPublishers.ofString(builder.toString());
}
Here is the code that sends the request to the server. First, it calls the method above to convert the parameters into a proper body and sets the Content-Type header.
var client = HttpClient.newHttpClient();
Map<Object, Object> data = new HashMap<>();
data.put("id", 1);
data.put("name", "a name");
data.put("ts", System.currentTimeMillis());
var request = HttpRequest.newBuilder()
.POST(ofFormData(data))
.uri(URI.create("https://localhost:8443/formdata"))
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
return client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.exceptionally(e -> "Error: " + e.getMessage())
.thenAccept(System.out::println);
Compression ¶
As mentioned initially, the Java 11 HTTP client does not handle compressed responses, nor does it send the Accept-Encoding
request header to request compressed responses by default.
If we know that the server can send back compressed resources, we can request them by adding the Accept-Encoding
header. We only want a compressed response in the gzip format in this example.
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.GET()
.header("Accept-Encoding", "gzip")
.uri(URI.create("https://localhost:8443/indexWithoutPush"))
.build();
The server can disregard this header and send back an uncompressed response, or he complies and sends back gzip-compressed resources. We have to handle both cases in our application unless you are sure that a server always sends back compressed resources.
The application reads the Content-Encoding
response header to check if a resource is compressed. If this header is present and contains the value gzip
,
the application uses the built-in GZIPInputStream
to decompress the response body. Otherwise, the resource is uncompressed, and no special handling is needed.
HttpResponse<InputStream> response = client.send(request, BodyHandlers.ofInputStream());
String encoding = response.headers().firstValue("Content-Encoding").orElse("");
if (encoding.equals("gzip")) {
System.out.println("gzip compressed");
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (InputStream is = new GZIPInputStream(response.body()); var autoCloseOs = os) {
is.transferTo(autoCloseOs);
}
System.out.println(new String(os.toByteArray(), StandardCharsets.UTF_8));
}
else {
System.out.println("not compressed");
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (var is = response.body(); var autoCloseOs = os) {
is.transferTo(autoCloseOs);
}
System.out.println(new String(os.toByteArray(), StandardCharsets.UTF_8));
}
Query Parameters ¶
Parameters are key/value pairs that are added to the URI. For example, https://duckduckgo.com/?q=java+11
sends a query parameter q
to the server with the value java+11
.
Notice that you also need to encode these parameters correctly. The built-in method
URLEncoder.encode
helps you with this conversion.
As mentioned initially, there is no built-in URI builder that provides an easy way to create URIs programmatically.
The only built-in way is by concatenating Strings and encoding them with URLEncoder
. Unfortunately, this is not very convenient when you work with many parameters.
Fortunately, a lightweight, dependency-free library is available, urlbuilder
, including a URI builder. You add it with the following coordinates to your project.
<dependency>
<groupId>io.mikael</groupId>
<artifactId>urlbuilder</artifactId>
<version>2.0.9</version>
</dependency>
Usage is straightforward. Every part of a URI can be specified separately, and the values are automatically encoded if required.
URI uri = UrlBuilder.empty()
.withScheme("https")
.withHost("localhost")
.withPort(8443)
.withPath("helloworld")
.addParameter("query", "value")
.toUri();
request = HttpRequest.newBuilder(uri).build();
An alternative is the org.springframework.web.util.UriComponentsBuilder
class from the Spring framework. If your application already depends on Spring, you can use this class and don't need to add any additional library.
URI uri = org.springframework.web.util.UriComponentsBuilder.newInstance()
.scheme("https")
.host("localhost")
.port(8443)
.path("helloworld")
.queryParam("query", "value")
.build()
.toUri();
Headers ¶
Headers are an essential part of the HTTP protocol. They transfer metadata about requests and responses.
Request ¶
The request builder provides three methods for adding headers: header
, headers
and setHeader
.
header
adds one header to the request. You can add the same header key multiple times, and they will be all sent to the server.
If you need to add many headers, you may find headers
convenient. You add multiple headers by passing the keys and values as arguments to the method
and always alternate between key and value.
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/headers"))
.header("X-Auth", "authtoken")
.headers("X-Custom1", "value1", "X-Custom2", "value2")
.setHeader("X-Auth", "overwrite authtoken")
.build();
headers
also allow you to add multiple values to one key. You have to supply the same key name with each new value.
.headers("X-MyHeader", "one", "X-MyHeader", "two", "X-MyHeader", "three")
// equivalent to
.header("X-MyHeader", "one").header("X-MyHeader", "two").header("X-MyHeader", "three")
setHeader
adds one key/value pair to the request headers, and it overwrites the previously set header with the same name. In the example above, the setHeader()
call overwrites the header from the header()
call.
Response ¶
The response provides the method headers()
to access the response headers.
This method returns an instance of HttpHeaders
, a read-only view of the headers.
allValues()
returns all values for a given header as an unmodifiable List. The method always returns a List and may be empty if there is no header with the given name in the response.
firstValue()
and
firstValueAsLong()
return an Optional containing the first value of a given header. If the header is not present, the Optional is empty. firstValueAsLong()
in addition to
retrieving the header, it also converts the value to a Long. This method throws a NumberFormatException if a value is present but can't be parsed to a Long.
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
for (String value : response.headers().allValues("X-Custom-Header")) {
System.out.println(value);
}
String firstValue = response.headers().firstValue("X-Custom-Header").orElse("");
long time = response.headers().firstValueAsLong("X-Time").orElse(-1L);
Cookies ¶
Cookies are a way to add state to HTTP, a stateless protocol by design. However, with Cookies, the server can associate multiple requests to the same session.
From a technical standpoint, cookies are just HTTP headers: Cookie
(request) and Set-Cookie
(response). However, they are treated specially by the browsers.
When a server wants to set a cookie, he adds the Set-Cookie
header to the response.
The client reads the value of this header and sends it in the Cookie
header with each consecutive request back to the server.
The Java 11 HttpClient
has built-in cookie support, but it's disabled by default.
To enable it, you use the following code.
CookieHandler.setDefault(new CookieManager());
var client = HttpClient.newBuilder()
.cookieHandler(CookieHandler.getDefault())
.build();
//OR
/*
var client = HttpClient.newBuilder()
.cookieHandler(new CookieManager())
.build();
*/
The default constructor creates a cookie manager that stores all cookies in RAM. You can change this behavior by instantiating the manager with
the other constructor (CookieManager(CookieStore store, CookiePolicy cookiePolicy)
) and specify an implementation of the CookieStore interface.
With the CookieManager in place, no special request or response handling is necessary. Everything is handled by the HTTP client transparently. The /setCookie
endpoint sets a cookie, and the client sends this cookie together with the subsequent request to secondCookieRequest
.
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/setCookie"))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.headers().firstValue("set-cookie"));
request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/secondCookieRequest"))
.build();
response = client.send(request, BodyHandlers.ofString());
Redirect ¶
Redirects are a signal from the server to the client that a resource has been moved to another location. Browsers automatically send another request to the new location when they receive a redirect response (301, 302, 303, 307, 308).
For the following examples, I wrote a server endpoint /redirect
that returns 308 PERMANENT_REDIRECT
and a new location /helloworld
.
NEVER ¶
By default, the redirect policy is set to Redirect.NEVER
which tells the HttpClient
not to follow any redirects.
In this mode, the application is responsible for handling redirect responses.
For example, the following request sends back a status code of 308. The application could now read the new location from the Location
response header,
maybe update the URI stored somewhere in a database and issue another GET request to the new location.
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/redirect"))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
int sc = response.statusCode();
String newLocation = response.headers().firstValue("Location").orElse(null);
NORMAL ¶
With the Redirect.NORMAL
mode, the HTTP client automatically follows redirects, except HTTPS to HTTP.
The client automatically sends another request if he receives a redirect response.
An application can check if a redirect occurred by comparing the URI stored in the response object (response.uri()
)
with the request URI.
In this example, the response URI is https://localhost:8443/helloworld
, and because it is different from the request URI the application knows that a redirect occurred.
var client = HttpClient.newBuilder().followRedirects(Redirect.NORMAL).build();
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/redirect"))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
int sc = response.statusCode();
String body = response.body();
URI uri = response.uri();
ALWAYS ¶
Redirect.ALWAYS
behaves like NORMAL
but in addition redirects from HTTPS URLs to HTTP URLs.
Basic Authentication ¶
Basic Authentication is a simple way to protect resources on the server. If a client accesses such resources without any authentication, the server sends back a status code of 401. The client then re-sends the request with an authentication header attached to it.
See the MDN documentation for a more in-depth look at Basic Authentication:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
This is all handled transparently by the Java 11 HTTP client when an Authenticator
instance is configured. You can do this in two ways.
var client = HttpClient.newBuilder()
.authenticator(new BasicAuthenticator("user", "password"))
.build();
/* OR
Authenticator.setDefault(new BasicAuthenticator("user", "password"));
var client = HttpClient.newBuilder()
.authenticator(Authenticator.getDefault())
.build();
*/
The application then requests a protected resource like any other unprotected resource.
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/secret"))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
Files ¶
HTTP is not limited to just sending and receiving text bodies. It is also capable of transferring binary data (image, audio, video, ...). The following example downloads a file and then uploads it to Virus Total. A service that analyses files to detect malware. The service provides a web interface where users upload files via a browser, and an HTTP API that an application can access with any HTTP client.
Download ¶
Downloading a file is very straightforward. First, send a GET request and then handle the bytes in the response according to your use case. In this example, we utilize a BodyHandler that automatically saves the bytes from the response into a file on the local filesystem.
var url = "https://www.7-zip.org/a/7z1806-x64.exe";
var client = HttpClient.newBuilder().build();
var request = HttpRequest.newBuilder().GET().uri(URI.create(url)).build();
Path localFile = Paths.get("7z.exe");
HttpResponse<Path> response = client.send(request, BodyHandlers.ofFile(localFile));
Notice that the Java 11 client does not handle compression transparently. See the compression example above. In this case, we download an installer for Windows, which is already compressed.
Upload with multipart ¶
If the server endpoint expects binary data in the request body, an application could send a POST request with BodyPublishers.ofFile
.
This publisher reads a file from the filesystem and sends the bytes in the body to the server.
However, in this case, we need to send some additional data in the POST request body and use a multipart form post with the Content-Type
multipart/form-data
. The request body is specially formatted as a series of parts, separated by boundaries. Unfortunately, the Java 11 HTTP client
does not provide convenient support for this body, but we can build it from scratch.
The following method takes a Map of key/value pairs and a boundary and builds the multipart body.
public static BodyPublisher ofMimeMultipartData(Map<Object, Object> data,
String boundary) throws IOException {
var byteArrays = new ArrayList<byte[]>();
byte[] separator = ("\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=")
.getBytes(StandardCharsets.UTF_8);
for (Map.Entry<Object, Object> entry : data.entrySet()) {
byteArrays.add(separator);
if (entry.getValue() instanceof Path) {
var path = (Path) entry.getValue();
String mimeType = Files.probeContentType(path);
byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName()
+ "\"\r\nContent-Type: " + mimeType + "\r\n\r\n")
.getBytes(StandardCharsets.UTF_8));
byteArrays.add(Files.readAllBytes(path));
}
else {
byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue())
.getBytes(StandardCharsets.UTF_8));
}
}
byteArrays.add(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
return BodyPublishers.ofByteArrays(byteArrays);
}
VirusTotal requires an API key to access the service. You get your key by joining the VirusTotal community.
The application then prepares the Map with all parameters we have to send to VirusTotal. In this case, the API key and the file. As a multipart boundary, a random 256 length string is used.
String virusTotalApiKey = "......................";
Map<Object, Object> data = new LinkedHashMap<>();
data.put("file", localFile);
String boundary = new BigInteger(256, new Random()).toString();
request = HttpRequest.newBuilder()
.header("Content-Type", "multipart/form-data;boundary=" + boundary)
.header("x-apikey", virusTotalApiKey).POST(ofMimeMultipartData(data, boundary))
.uri(URI.create("https://www.virustotal.com/api/v3/files")).build();
The VirusTotal API sends back a JSON. However, you don't get a scan result immediately because the scanning process takes several minutes. The API sends back a resource token that you can use to access the result. An application needs to periodically poll VirusTotal with this resource token to fetch the result of the malware scan. This is not shown here. The example prints out the initial response of the upload.
HTTP/2 Server Push ¶
HTTP/2 Server Push is a new way to send resources from a server to the client. Traditionally a browser requests an HTML page parses the code and sends additional requests for all the referenced resources on the page (JS, CSS, images, ...)
With HTTP/2 Server Push, a server sends back the HTML and sends back all the referenced resources that the browser needs to display the page.
In the following example, the /indexWithPush
endpoint
not only send back an HTML page but also a picture referenced on the page.
To handle these resources, an application has to implement the PushPromiseHandler
interface and then pass an instance of this implementation as the third argument to the send()
or sendAsync()
method.
request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://localhost:8443/indexWithPush"))
.build();
var asyncRequests = new CopyOnWriteArrayList<CompletableFuture<Void>>();
PushPromiseHandler<byte[]> pph = (initial, pushRequest, acceptor) -> {
CompletableFuture<Void> cf = acceptor.apply(BodyHandlers.ofByteArray())
.thenAccept(response -> {
System.out.println("Got pushed resource: " + response.uri());
System.out.println("Body: " + response.body());
});
asyncRequests.add(cf);
};
client.sendAsync(request, BodyHandlers.ofByteArray(), pph)
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
WebSocket ¶
Lastly, we look at a WebSocket example.
First, we need to implement a WebSocket Listener. This is an interface composed of several methods, but default methods implement all methods. Therefore you only have to implement the methods your application needs for its work. For example, in this demo, we listen for the open and close event, and in onText, print any text message, the server sends it to us.
Listener wsListener = new Listener() {
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence data, boolean last) {
System.out.println("onText: " + data);
return Listener.super.onText(webSocket, data, last);
}
@Override
public void onOpen(WebSocket webSocket) {
System.out.println("onOpen");
Listener.super.onOpen(webSocket);
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode,
String reason) {
System.out.println("onClose: " + statusCode + " " + reason);
return Listener.super.onClose(webSocket, statusCode, reason);
}
};
Every WebSocket connection starts with a HTTP request. Similar to this we have to first create a HttpClient and
then call newWebSocketBuilder()
.buildAsync()
to build a WebSocket instance asynchronously.
You can only build a WebSocket connection asynchronously. There is no blocking method available.
This example blocks the calling thread with join()
until it gets the WebSocket client. Not something you should do in a production application if you are writing your application in a non-blocking fashion.
var client = HttpClient.newHttpClient();
WebSocket webSocket = client.newWebSocketBuilder()
.buildAsync(URI.create("wss://localhost:8443/wsEndpoint"), wsListener).join();
webSocket.sendText("hello from the client", true);
TimeUnit.SECONDS.sleep(30);
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
With the WebSocket instance the application can now send messages to the server. Text messages with sendText()
and binary messages with sendBinary
sendClose()
sends a close message to the server with a reason code. This
method does not close the connection, and it just initiates a proper shutdown of the connection. Usually, a server closes the connection after receiving the close message.
To immediately and forcefully close a WebSocket connection you may call abort()
.
Notice that the Java 11 WebSocket client, like the WebSocket API in the browser, does not automatically reconnect to a server
when the connection breaks. Therefore, if your application requires a permanent connection, you need to build a reconnection mechanism on top of the WebSocket client. For example, you could start a new WebSocket connection from the onClose
event or implement a more robust mechanism by sending heartbeat messages. If they don't arrive, tear down the connection and rebuild it.
This concludes our Java 11 HTTP client tour. You have seen a lot of examples and implementations for the missing convenient functionality for multipart, form data, URI builder, and compression. With the new HTTP client, Java finally has a robust HTTP client implementation built-in to the core. As a result, you no longer need to add an external HTTP client library to your project in many cases.