HttpClient in Java
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
| Type | Role | Created with |
|---|---|---|
HttpClient | Sends requests. Holds shared settings and connections. Create one and reuse it. | HttpClient.newHttpClient() or HttpClient.newBuilder()...build() |
HttpRequest | One 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
| Method | Description |
|---|---|
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 helper | Purpose |
|---|---|
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.
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);
}
}
}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_1Explanation:
- The client is built once, with a connect timeout and a redirect policy, in try-with-resources (an
HttpClientcan be closed since Java 21). - GET: the request is built with an address, a header and a timeout.
sendblocks until the whole response has arrived.BodyHandlers.ofString()says "give me the body as aString", sopage.body()is aString. The title is pulled out with a small regular expression. - POST:
BodyPublishers.ofString(json)supplies the body. The server answers 201. - 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.
sendAsyncstarts all three requests without waiting, each returning aCompletableFuture.thenApply(HttpResponse::body)transforms each result, andjoin()collects them in order. The three requests are in flight at the same time (see "CompletableFuture in Java").- Timeout: the
/slowpath takes 1.5 seconds but the request allows 300 ms, sosendthrowsHttpTimeoutException.
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:
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"));
}Status: 200
Version: HTTP_2
Title: Example DomainBlocking or Asynchronous?
| Use | When |
|---|---|
send | Simple scripts, or code already running on its own thread. With virtual threads, blocking calls are cheap, so send is often all you need. |
sendAsync | Several 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
- Easy: Add a request to
/marks/Navikathat saves the body to a file withBodyHandlers.ofFile(Path.of("marks.txt")). - Harder: Write a method that takes a list of addresses, fetches them all with
sendAsync, and returns a map from address to status code, usingCompletableFuture.allOf.
Key Takeaways
Was this tutorial helpful?