HttpClient in Java

Naveen AnamaneniUpdated · 8 min read

HttpClient is Java's modern API for making HTTP requests, added as a standard feature in Java 11 in the package java.net.http. It replaces HttpURLConnection with a design built on three immutable types: an HttpClient that sends requests, an HttpRequest that describes one, and an HttpResponse that holds the answer. It supports HTTP/1.1 and HTTP/2, both blocking and asynchronous calls, and ready-made ways to send and receive bodies. This is the class to use when a Java program calls a REST API or downloads a page. In this lesson, you will learn to build requests, send them both ways, post JSON, deal with errors and timeouts, and complete this module's second checkpoint: fetching a page and printing its status code and title.

The Three Types

TypeRoleCreated with
HttpClientSends requests. Holds shared settings and connections. Create one and reuse it.HttpClient.newHttpClient() or HttpClient.newBuilder()...build()
HttpRequestOne request: address, method, headers, body, timeout.HttpRequest.newBuilder(uri)...build()
HttpResponse<T>The answer: status code, headers, and a body of type T.Returned by send or sendAsync

Methods

MethodDescription
client.send(request, bodyHandler)It is used to send a request and wait for the response.
client.sendAsync(request, bodyHandler)It sends the request and returns a CompletableFuture at once.
HttpRequest.Builder.GET(), POST(publisher), PUT(publisher), DELETE()They choose the method and the body.
HttpRequest.Builder.header(name, value)It adds a request header.
HttpRequest.Builder.timeout(duration)It limits the time for the whole response to this request.
HttpClient.Builder.connectTimeout(duration)It limits the time to establish a connection.
HttpClient.Builder.followRedirects(policy)It sets whether redirects are followed.
response.statusCode(), headers(), body()They read the response.
Body helperPurpose
BodyPublishers.ofString(text), ofFile(path), ofByteArray(bytes), noBody()Supply the request body
BodyHandlers.ofString(), ofFile(path), ofByteArray(), ofLines(), ofInputStream(), discarding()Decide how to receive the response body

Checkpoint: Requests, Errors, Async and a Page Title

As before, the program starts a small local server first, so it runs without internet access. Everything inside the try in main is what your own code would look like.

Client.java▶ Try it
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
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.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class Client {
    static HttpServer startServer() throws Exception {
        HttpServer server =
            HttpServer.create(new InetSocketAddress("localhost", 0), 0);
        server.createContext("/", exchange -> {
            String path = exchange.getRequestURI().getPath();
            int status = 200;
            String type = "text/html; charset=utf-8";
            String body;
            if (path.equals("/")) {
                body = "<html><head><title>PoolString Test Page</title></head>"
                    + "<body>Hello</body></html>";
            } else if (path.equals("/students")
                    && exchange.getRequestMethod().equals("POST")) {
                String sent = new String(
                    exchange.getRequestBody().readAllBytes(),
                    StandardCharsets.UTF_8);
                status = 201;
                type = "application/json";
                body = "{\"saved\":" + sent + "}";
            } else if (path.startsWith("/marks/")) {
                type = "text/plain";
                body = path.substring(7) + " scored "
                    + (70 + path.length());
            } else if (path.equals("/slow")) {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                body = "late";
            } else {
                status = 404;
                body = "not found";
            }
            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().set("Content-Type", type);
            exchange.sendResponseHeaders(status, bytes.length);
            exchange.getResponseBody().write(bytes);
            exchange.close();
        });
        server.setExecutor(
            Executors.newVirtualThreadPerTaskExecutor());
        server.start();
        return server;
    }
 
    public static void main(String[] args) throws Exception {
        HttpServer server = startServer();
        String base = "http://localhost:" + server.getAddress().getPort();
        try (HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(3))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .build()) {
 
            // 1. A simple GET: status code, a header and the page title.
            HttpRequest get = HttpRequest.newBuilder(URI.create(base + "/"))
                .header("User-Agent", "PoolString-Lesson")
                .timeout(Duration.ofSeconds(3))
                .GET()
                .build();
            HttpResponse<String> page =
                client.send(get, HttpResponse.BodyHandlers.ofString());
            System.out.println("Status: " + page.statusCode());
            System.out.println("Type:   " + page.headers()
                .firstValue("content-type").orElse("?"));
            Matcher m = Pattern.compile("<title>(.*?)</title>")
                .matcher(page.body());
            System.out.println("Title:  " + (m.find() ? m.group(1) : "none"));
 
            // 2. A POST with a JSON body.
            HttpRequest post = HttpRequest
                .newBuilder(URI.create(base + "/students"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                    "{\"name\":\"Advitha\"}"))
                .build();
            HttpResponse<String> created =
                client.send(post, HttpResponse.BodyHandlers.ofString());
            System.out.println(created.statusCode() + " " + created.body());
 
            // 3. A 404 is a normal response, not an exception.
            HttpResponse<String> missing = client.send(
                HttpRequest.newBuilder(URI.create(base + "/nope")).build(),
                HttpResponse.BodyHandlers.ofString());
            System.out.println(missing.statusCode() + " " + missing.body());
 
            // 4. Three requests at once with sendAsync.
            List<CompletableFuture<String>> futures =
                List.of("Navika", "Srihitha", "Advitha").stream()
                    .map(name -> HttpRequest.newBuilder(
                        URI.create(base + "/marks/" + name)).build())
                    .map(req -> client
                        .sendAsync(req, HttpResponse.BodyHandlers.ofString())
                        .thenApply(HttpResponse::body))
                    .toList();
            for (CompletableFuture<String> f : futures) {
                System.out.println(f.join());
            }
 
            // 5. A request that takes too long.
            HttpRequest slow = HttpRequest
                .newBuilder(URI.create(base + "/slow"))
                .timeout(Duration.ofMillis(300))
                .build();
            try {
                client.send(slow, HttpResponse.BodyHandlers.ofString());
            } catch (HttpTimeoutException e) {
                System.out.println("HttpTimeoutException: " + e.getMessage());
            }
            System.out.println("HTTP version used: " + page.version());
        } finally {
            server.stop(0);
        }
    }
}
Output
Status: 200
Type:   text/html; charset=utf-8
Title:  PoolString Test Page
201 {"saved":{"name":"Advitha"}}
404 not found
Navika scored 83
Srihitha scored 85
Advitha scored 84
HttpTimeoutException: request timed out
HTTP version used: HTTP_1_1

Explanation:

  1. The client is built once, with a connect timeout and a redirect policy, in try-with-resources (an HttpClient can be closed since Java 21).
  2. GET: the request is built with an address, a header and a timeout. send blocks until the whole response has arrived. BodyHandlers.ofString() says "give me the body as a String", so page.body() is a String. The title is pulled out with a small regular expression.
  3. POST: BodyPublishers.ofString(json) supplies the body. The server answers 201.
  4. A 404 is not an exception. The response arrives normally; you decide what a status code means. Exceptions are only for failures to get any response, such as a refused connection or a timeout.
  5. sendAsync starts all three requests without waiting, each returning a CompletableFuture. thenApply(HttpResponse::body) transforms each result, and join() collects them in order. The three requests are in flight at the same time (see "CompletableFuture in Java").
  6. Timeout: the /slow path takes 1.5 seconds but the request allows 300 ms, so send throws HttpTimeoutException.

The local test server speaks HTTP/1.1. Against a real HTTPS site, the client uses HTTP/2 when the server offers it.

Against a Real Website

The same code with a real address:

Real.java
try (HttpClient client = HttpClient.newHttpClient()) {
    HttpResponse<String> page = client.send(
        HttpRequest.newBuilder(URI.create("https://example.com/")).build(),
        HttpResponse.BodyHandlers.ofString());
    Matcher m = Pattern.compile("<title>(.*?)</title>").matcher(page.body());
    System.out.println("Status:  " + page.statusCode());
    System.out.println("Version: " + page.version());
    System.out.println("Title:   " + (m.find() ? m.group(1) : "none"));
}
Terminal output
Status:  200
Version: HTTP_2
Title:   Example Domain

Blocking or Asynchronous?

UseWhen
sendSimple scripts, or code already running on its own thread. With virtual threads, blocking calls are cheap, so send is often all you need.
sendAsyncSeveral requests at once from one place, or pipelines that combine results with thenApply, thenCombine and allOf.

Sending JSON

HttpClient deals in text and bytes; it doesn't convert objects to JSON. Produce the JSON with a library such as Jackson, then send it with BodyPublishers.ofString(json) and the header Content-Type: application/json. On the way back, read the body with BodyHandlers.ofString() and parse it.

Common Mistakes

Creating a new client for every request. Connections can't be reused.

Treating a returned response as success. A 500 comes back as a response; check statusCode().

No timeouts. Set connectTimeout on the client and timeout on each request.

Calling join() on each future immediately after sendAsync. That waits for one request before starting the next, which is just send with extra steps. Start them all first, then join.

Interview Corner

What is HttpClient and what are its advantages?

java.net.http.HttpClient, standard since Java 11, is the JDK's modern HTTP client. A reusable, immutable HttpClient sends immutable HttpRequest objects and returns HttpResponse<T>. It supports HTTP/1.1 and HTTP/2, synchronous (send) and asynchronous (sendAsync, returning CompletableFuture) calls, WebSocket, per-request and connection timeouts, redirect policies, authentication, and body publishers and handlers for strings, bytes, files and streams. Unlike HttpURLConnection, error statuses are ordinary responses.

What is the difference between send and sendAsync?

send blocks the calling thread until the response is complete and returns HttpResponse<T>, throwing IOException or InterruptedException on failure. sendAsync returns immediately with a CompletableFuture<HttpResponse<T>> that completes later, so many requests can run concurrently and be composed with thenApply, thenCompose or allOf.

Practice

  1. Easy: Add a request to /marks/Navika that saves the body to a file with BodyHandlers.ofFile(Path.of("marks.txt")).
  2. Harder: Write a method that takes a list of addresses, fetches them all with sendAsync, and returns a map from address to status code, using CompletableFuture.allOf.

Quick check: HttpClient

4 questions
1Which type actually sends a request?
2What does sendAsync return?
3The server answers 404. What does send do?
4How many HttpClient objects should an application usually create?

Key Takeaways

Was this tutorial helpful?

Lesson 481 of 482