Java 11 HTTP Client API Tutorial with Examples
Introduction
Java 11 (JEP 321) added a modern HTTP Client API to the JDK, in the
(Note - The asynchronous examples use
A local server for the examples
So that you can run every example without depending on a website, the examples call a small server running on your own machine. It is built with the simple HTTP server which comes with the JDK, in the
Sending a synchronous GET request
OUTPUT of the above code
Explanation of the code
Sending a POST request with a body
For POST and PUT requests, the body is supplied with a
OUTPUT of the above code
Note - The JDK has no JSON library, so the JSON here is written as a string. In real applications you would create it, and parse JSON responses, with a library such as Jackson or Gson. Also note that
OUTPUT of the above code
Explanation of the code
OUTPUT of the above code
Explanation of the code
java.net.http package. It had been available as an incubator module in Java 9 and 10, and became a standard part of Java SE in Java 11. This tutorial explains the Java 11 HTTP Client with examples. We will first see why a new client was needed, and the three main classes of the API. We will then send synchronous GET and POST requests, and handle responses with body handlers. Next we will send several requests in parallel with sendAsync(). Lastly, we will configure redirects, timeouts and the HTTP version.(Note - The asynchronous examples use
CompletableFuture, lambda expressionsClick to read: lambda expressions and method referencesClick to read: method references.)
Why a new HTTP client
Before Java 11, the JDK's only HTTP client was HttpURLConnection, which dates back to Java 1.1. It was designed for many protocols, works only in a blocking style, supports only HTTP/1.1, and is awkward to use - setting a request body, for example, means writing to an output stream. Most projects used a library such as Apache HttpClient or OkHttp instead. The new client -
- has a fluent, immutable builder-based API,
- supports both synchronous (blocking) and asynchronous (non-blocking) requests,
- supports HTTP/2, falling back to HTTP/1.1 when the server does not support it, and
- supports WebSocket, through
HttpClient.newWebSocketBuilder().
- HttpClient - sends requests and receives responses. It holds the configuration shared by requests, such as the HTTP version, redirect policy, proxy, authentication and connection pool. A client is immutable and thread-safe, so create one and reuse it for all requests.
- HttpRequest - an immutable request, built with
HttpRequest.newBuilder(). It has the URI, method, headers, body and timeout. Request bodies are supplied by aBodyPublisher, created with methods such asBodyPublishers.ofString()andBodyPublishers.ofFile(). - HttpResponse<T> - the response, with its status code, headers and body. A
BodyHandler, passed when sending the request, decides what typeTthe body is converted to - for exampleBodyHandlers.ofString()gives aStringandBodyHandlers.ofFile()saves it to a file.
com.sun.net.httpserver package. You do not need to understand this class to follow the tutorial - just note the four paths it serves.
A local server for trying out the HTTP client
//LocalServer.java
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
public class LocalServer {
public static HttpServer start() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8811), 0);
// GET /hello - a plain text greeting
server.createContext("/hello", ex -> send(ex, 200, "Hello from the local server!"));
// POST /orders - echoes the request body and its content type
server.createContext("/orders", ex -> {
String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
send(ex, 201, "Created order from " + ex.getRequestHeaders().getFirst("Content-Type") + ": " + body);
});
// GET /slow?... - answers after half a second
server.createContext("/slow", ex -> {
try { Thread.sleep(500); } catch (InterruptedException e) { }
send(ex, 200, "Slow reply for " + ex.getRequestURI().getQuery());
});
// GET /old-hello - permanently moved to /hello
server.createContext("/old-hello", ex -> {
ex.getResponseHeaders().add("Location", "/hello");
ex.sendResponseHeaders(301, -1);
ex.close();
});
server.setExecutor(Executors.newFixedThreadPool(4, r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}));
server.start();
return server;
}
static void send(HttpExchange ex, int status, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
ex.sendResponseHeaders(status, bytes.length);
try (OutputStream os = ex.getResponseBody()) {
os.write(bytes);
}
}
}
A synchronous GET request
//HttpGetDemo.java
import com.sun.net.httpserver.HttpServer;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpGetDemo {
public static void main(String[] args) throws Exception {
HttpServer server = LocalServer.start();
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8811/hello"))
.header("Accept", "text/plain")
.GET() // GET is the default, so this line is optional
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Content-Type: " + response.headers().firstValue("Content-Type").orElse("none"));
System.out.println("Body: " + response.body());
// An error status is a normal response, not an exception
HttpResponse<String> missing = client.send(
HttpRequest.newBuilder(URI.create("http://localhost:8811/missing")).build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("Status for /missing: " + missing.statusCode());
server.stop(0);
}
}
Status: 200 Content-Type: text/plain; charset=utf-8 Body: Hello from the local server! Status for /missing: 404
HttpClient.newHttpClient()creates a client with the default settings. We will see how to configure one withHttpClient.newBuilder()later in this tutorial.- The request is built with
HttpRequest.newBuilder(), setting the URI, a header and the method.build()returns an immutableHttpRequest, which can be sent any number of times. client.send()blocks until the response arrives.BodyHandlers.ofString()converts the body to aString, using the charset given in theContent-Typeheader, soresponse.body()is aString.response.headers().firstValue()returns anOptional<String>, since the header may not be present.- A 404 or 500 status does not throw an exception. Always check
statusCode().send()throws anIOExceptiononly when there is no response at all, for example when the server cannot be reached.
BodyPublisher. BodyPublishers.ofString() sends a string; there are also ofByteArray(), ofFile(), ofInputStream() and noBody().
A POST request with a JSON body
//HttpPostDemo.java
import com.sun.net.httpserver.HttpServer;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpPostDemo {
public static void main(String[] args) throws Exception {
HttpServer server = LocalServer.start();
HttpClient client = HttpClient.newHttpClient();
String json = "{\"item\":\"Java 11 book\",\"quantity\":2}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8811/orders"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(request.method() + " " + request.uri() + " -> " + response.statusCode());
System.out.println(response.body());
server.stop(0);
}
}
POST http://localhost:8811/orders -> 201
Created order from application/json: {"item":"Java 11 book","quantity":2}
BodyPublishers.ofString() does not set the Content-Type header for you.
Asynchronous requests with sendAsync()
sendAsync() sends a request without blocking, and immediately returns a CompletableFuture<HttpResponse<T>>, which completes when the response arrives. This makes it easy to send several requests at the same time. Each call to /slow takes half a second, so three requests sent one after the other would take 1.5 seconds -
Sending requests in parallel with sendAsync()
//HttpAsyncDemo.java
import com.sun.net.httpserver.HttpServer;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class HttpAsyncDemo {
public static void main(String[] args) throws Exception {
HttpServer server = LocalServer.start();
HttpClient client = HttpClient.newHttpClient();
long start = System.nanoTime();
List<CompletableFuture<String>> futures = List.of("a", "b", "c").stream()
.map(id -> HttpRequest.newBuilder(URI.create("http://localhost:8811/slow?id=" + id)).build())
.map(request -> client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body))
.collect(Collectors.toList());
System.out.println("All 3 requests sent, waiting for responses...");
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
futures.forEach(f -> System.out.println(f.join()));
long millis = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took less than 1 second: " + (millis < 1000));
server.stop(0);
}
}
All 3 requests sent, waiting for responses... Slow reply for id=a Slow reply for id=b Slow reply for id=c Took less than 1 second: true
- Each
sendAsync()call returns at once, which is why "All 3 requests sent" is printed before any response has arrived. thenApply(HttpResponse::body)transforms each future so that it completes with just the body.CompletableFuture.allOf()returns a future which completes when all three have completed, andjoin()waits for it. Since the three requests were handled at the same time, the total time is about half a second, not 1.5 seconds.- By default, the client runs its asynchronous work on an internal thread pool. You can pass your own with
HttpClient.newBuilder().executor(...).
Configuring an HttpClient
//HttpConfigDemo.java
import com.sun.net.httpserver.HttpServer;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
public class HttpConfigDemo {
public static void main(String[] args) throws Exception {
HttpServer server = LocalServer.start();
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // prefer HTTP/2 (this is the default)
.followRedirects(HttpClient.Redirect.NORMAL) // follow redirects (default is NEVER)
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = client.send(
HttpRequest.newBuilder(URI.create("http://localhost:8811/old-hello")).build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("Final URI: " + response.uri() + " -> " + response.statusCode());
System.out.println("Redirected from status: " + response.previousResponse().map(HttpResponse::statusCode).orElse(0));
System.out.println("HTTP version used: " + response.version());
System.out.println("Body: " + response.body());
HttpRequest tooSlow = HttpRequest.newBuilder(URI.create("http://localhost:8811/slow?id=timeout"))
.timeout(Duration.ofMillis(200))
.build();
try {
client.send(tooSlow, HttpResponse.BodyHandlers.ofString());
} catch (HttpTimeoutException e) {
System.out.println("Timed out: " + e.getMessage());
}
server.stop(0);
}
}
Final URI: http://localhost:8811/hello -> 200 Redirected from status: 301 HTTP version used: HTTP_1_1 Body: Hello from the local server! Timed out: request timed out
- By default the client does not follow redirects. With
Redirect.NORMAL, it follows them, except from HTTPS to HTTP. The request to/old-hellogot a 301 and was followed to/hello.response.uri()gives the final URI, andpreviousResponse()the response which caused the redirect. - The client asked for HTTP/2, but the local server supports only HTTP/1.1, so the client fell back to it - as
response.version()shows. Against a server which supports HTTP/2, such as most large websites over HTTPS, the same code uses HTTP/2. connectTimeout()on the client limits the time to open a connection, whiletimeout()on a request limits the time to receive the response. The/slowpath takes 500 ms, so the 200 ms request timeout throws anHttpTimeoutException.
ofString(), HttpResponse.BodyHandlers has handlers for most needs -
ofFile(Path)- writes the body to a file, which is how you download files. The body type is thePath.ofLines()- gives the body as aStream<String>of lines, read as the stream is consumed.ofInputStream()- gives anInputStream, for processing large bodies as they arrive.ofByteArray()- gives the body as abyte[], useful for images and other binary data.discarding()- ignores the body, when only the status and headers matter.
HttpClient has no close() method. Its resources are released when it is no longer used. Java 21 made HttpClient implement AutoCloseable.
Summary
In this tutorial we saw why Java 11 added a new HTTP client to replace HttpURLConnection, and its three main classes - HttpClient, HttpRequest and HttpResponse. We sent a synchronous GET request and saw that error statuses do not throw exceptions, and sent a POST request with a body publisher. We then sent several requests in parallel with sendAsync() and CompletableFuture. Lastly, we configured redirects, timeouts and HTTP/2, and looked at the other body handlers. In the next tutorial, we will look at the modules removed in Java 11Click to read tutorial: Java 11 Removed Modules: Java EE, CORBA and JavaFX and how to migrate code which used them.Continue Reading Java 11 Tutorials
Java 11 Removed Modules: Java EE, CORBA and JavaFXClick to read tutorial: Java 11 Removed Modules: Java EE, CORBA and JavaFX Java 11 API Additions: Files.readString, Predicate.notClick to read tutorial: Java 11 API Additions: Files.readString, Predicate.not Overview of New Features in Java 11Click to read tutorial: Overview of New Features in Java 11 Java 11 String Methods: isBlank, lines, strip, repeatClick to read tutorial: Java 11 String Methods: isBlank, lines, strip, repeat Java 11 var in Lambda Parameters with ExamplesClick to read tutorial: Java 11 var in Lambda Parameters with Examples Java 11: Run Single-File Source Code Programs with javaClick to read tutorial: Java 11: Run Single-File Source Code Programs with java
Java 11 Removed Modules: Java EE, CORBA and JavaFXClick to read tutorial: Java 11 Removed Modules: Java EE, CORBA and JavaFX Java 11 API Additions: Files.readString, Predicate.notClick to read tutorial: Java 11 API Additions: Files.readString, Predicate.not Overview of New Features in Java 11Click to read tutorial: Overview of New Features in Java 11 Java 11 String Methods: isBlank, lines, strip, repeatClick to read tutorial: Java 11 String Methods: isBlank, lines, strip, repeat Java 11 var in Lambda Parameters with ExamplesClick to read tutorial: Java 11 var in Lambda Parameters with Examples Java 11: Run Single-File Source Code Programs with javaClick to read tutorial: Java 11: Run Single-File Source Code Programs with java