Java 11 HTTP Client API Tutorial with Examples

Introduction Java 11 (JEP 321) added a modern HTTP Client API to the JDK, in the 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().
The main classes of the HTTP Client API
  • 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 a BodyPublisher, created with methods such as BodyPublishers.ofString() and BodyPublishers.ofFile().
  • HttpResponse<T> - the response, with its status code, headers and body. A BodyHandler, passed when sending the request, decides what type T the body is converted to - for example BodyHandlers.ofString() gives a String and BodyHandlers.ofFile() saves it to a file.
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 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);
    }
  }
}
Sending a synchronous GET request
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);
  }
}
 OUTPUT of the above code
Status: 200
Content-Type: text/plain; charset=utf-8
Body: Hello from the local server!
Status for /missing: 404
Explanation of the code
  • HttpClient.newHttpClient() creates a client with the default settings. We will see how to configure one with HttpClient.newBuilder() later in this tutorial.
  • The request is built with HttpRequest.newBuilder(), setting the URI, a header and the method. build() returns an immutable HttpRequest, which can be sent any number of times.
  • client.send() blocks until the response arrives. BodyHandlers.ofString() converts the body to a String, using the charset given in the Content-Type header, so response.body() is a String.
  • response.headers().firstValue() returns an Optional<String>, since the header may not be present.
  • A 404 or 500 status does not throw an exception. Always check statusCode(). send() throws an IOException only when there is no response at all, for example when the server cannot be reached.
Sending a POST request with a body For POST and PUT requests, the body is supplied with a 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);
  }
}
 OUTPUT of the above code
POST http://localhost:8811/orders -> 201
Created order from application/json: {"item":"Java 11 book","quantity":2}
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 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);
  }
}
 OUTPUT of the above code
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
Explanation of the code
  • 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, and join() 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 the client: redirects, timeouts and HTTP/2
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);
  }
}
 OUTPUT of the above code
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
Explanation of the code
  • By default the client does not follow redirects. With Redirect.NORMAL, it follows them, except from HTTPS to HTTP. The request to /old-hello got a 301 and was followed to /hello. response.uri() gives the final URI, and previousResponse() 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, while timeout() on a request limits the time to receive the response. The /slow path takes 500 ms, so the 200 ms request timeout throws an HttpTimeoutException.
Other body handlers Besides 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 the Path.
  • ofLines() - gives the body as a Stream<String> of lines, read as the stream is consumed.
  • ofInputStream() - gives an InputStream, for processing large bodies as they arrive.
  • ofByteArray() - gives the body as a byte[], useful for images and other binary data.
  • discarding() - ignores the body, when only the status and headers matter.
Note - In Java 11, 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.