The Java HTTP Client (java.net.http)
Reviewed & published by Brayan K
The java.net.http client is Java's modern, built-in way to make HTTP calls — a fluent HttpClient that supports synchronous and asynchronous requests, HTTP/2, headers, timeouts, and pluggable response handlers.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should be comfortable with exceptions (these calls throw checked ones), and it helps to have seen lambdas for the async CompletableFuture chains. The client ships in Java 11+.
What You'll Learn
- Creating an HttpClient and building requests fluently
- Synchronous send vs asynchronous sendAsync
- BodyHandlers: ofString, ofByteArray, ofFile and more
- Adding headers and per-request timeouts
- Reading the status code and response headers
- Why a 404 is a status, not an exception
The Big Idea — Posting a Letter
💡 Analogy: An HttpRequest is an envelope: the uri is the address, the headers are the notes on the front (what's inside, who sent it), and the body is the letter. The HttpClient is the postal service: send means you wait at the post office for the reply, while sendAsync means you go home and get notified when it lands. A BodyHandler is your instruction for what to do with the reply — read it aloud (ofString), file it (ofFile), or shred it (discarding).
All the examples below talk to a tiny local server we start inside the same program on 127.0.0.1 with an OS-chosen free port — so they really run and print real output, with no internet needed.
1️⃣ A Synchronous Request
The simplest flow: create a client, build a request, and send it with a body handler. The example spins up a com.sun.net.httpserver.HttpServer on a random local port, then calls it — so it runs fully offline and prints the real status, a response header, and the body.
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
// 1. Start a tiny local server on 127.0.0.1 with an OS-chosen free port.
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/hello", exchange -> {
byte[] body = "Hello from the server!".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("X-Demo", "java-http");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(body); }
});
server.start();
int port = server.getAddress().getPort();
// 2. Build a client and a request.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + port + "/hello"))
.header("Accept", "text/plain")
.GET()
.build();
// 3. Send synchronously and read the body as a String.
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("X-Demo header: " + response.headers().firstValue("X-Demo").orElse("?"));
System.out.println("Body: " + response.body());
server.stop(0);
}
}2️⃣ The Request Builder in Detail
HttpRequest.newBuilder() returns a fluent builder. Chain uri, one or more header calls, a timeout, and a method (GET(), POST(publisher), PUT(...), DELETE()), then build(). The result is immutable and reusable. You can build and inspect a request entirely offline:
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class Main {
public static void main(String[] args) {
// You can BUILD and inspect a request fully offline — no call is sent.
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/v1/users"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "application/json")
.header("Authorization", "Bearer TOKEN123")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"Ana\"}"))
.build();
System.out.println("Method: " + request.method());
System.out.println("URI: " + request.uri());
System.out.println("Timeout: " + request.timeout().map(Duration::toString).orElse("none"));
System.out.println("Accept: " + request.headers().firstValue("Accept").orElse("?"));
System.out.println("Has Authorization header? "
+ request.headers().firstValue("Authorization").isPresent());
}
}3️⃣ Asynchronous with sendAsync
sendAsync returns a CompletableFuture instead of blocking. You can transform it with thenApply (here, pull out the body), chain further calls with thenCompose, and only block (via join / get) when you finally need the value. Below we POST a body and the server echoes it back.
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
// Echo back whatever was POSTed.
server.createContext("/echo", exchange -> {
byte[] in = exchange.getRequestBody().readAllBytes();
String reply = "You sent: " + new String(in, StandardCharsets.UTF_8);
byte[] body = reply.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(body); }
});
server.start();
int port = server.getAddress().getPort();
HttpClient client = HttpClient.newHttpClient();
HttpRequest post = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + port + "/echo"))
.header("Content-Type", "text/plain")
.POST(HttpRequest.BodyPublishers.ofString("ping"))
.build();
// sendAsync returns a CompletableFuture — non-blocking.
CompletableFuture<String> future = client
.sendAsync(post, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
// join() waits for the async result here so output is deterministic.
System.out.println("Async result: " + future.join());
server.stop(0);
}
}🧠 Quick Recall
Predict before you peek.
Answer: no. send returns normally with a populated HttpResponse; you must check response.statusCode() == 404 yourself. Only transport failures (no connection, timeout, bad host) throw.
Answer: a CompletableFuture<HttpResponse<T>>. You chain it with thenApply/thenAccept and block only when you call join() or get().
Answer: HttpResponse.BodyHandlers.ofFile(path). The response's body() then returns the Path it wrote to.
🎯 YOUR TURN — Uppercase service
Register a /shout endpoint that uppercases its text query parameter, then call it and print the result.
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
// YOUR TURN: register a "/shout" context that reads the query string
// (e.g. "text=hello"), and responds with the text in UPPER CASE.
// Then build a GET request to /shout?text=hello, send it, and print
// the status and body.
//
// Expected output:
// Status: 200
// Body: HELLO
server.start();
int port = server.getAddress().getPort();
// build client + request, send, print, then server.stop(0);
}
}🧩 MINI-CHALLENGE — Handle status codes
Send two requests and branch on the status: 200 prints the body, anything else prints the error code. This is the everyday pattern for real APIs.
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
// MINI-CHALLENGE: the server returns 200 with JSON for "/data" but
// 404 for any other path. Send TWO requests — "/data" and "/missing"
// — and for each, print "OK, body: ..." on 200 or "ERROR <code>"
// otherwise. Loop over both paths.
//
// Expected output:
// /data -> OK, body: {"value":42}
// /missing -> ERROR 404
// your code here
}
}Common Errors
- ❌ Treating a 404/500 as a thrown error. It isn't. Fix: inspect response.statusCode() and branch.
- ❌ Forgetting the BodyHandler. send needs to know how to read the body. Fix: pass BodyHandlers.ofString() (or another handler).
- ❌ No timeout. A hung server can block forever. Fix: add .timeout(Duration.ofSeconds(n)).
- ❌ Not handling the checked exceptions. send throws IOException and InterruptedException. Fix: declare or catch them.
- ❌ Expecting auto-JSON. The client moves bytes, not objects. Fix: serialise with Jackson/Gson, then pass the JSON string to ofString.
- ❌ Creating a new client per call. Wasteful. Fix: build one HttpClient and reuse it; it is thread-safe.
Pro Tips
- 💡 Reuse one client. HttpClient is immutable and thread-safe — share it across your app.
- 💡 Set an HTTP version with HttpClient.newBuilder().version(HttpClient.Version.HTTP_2) when you need it.
- 💡 Stream large responses with BodyHandlers.ofLines() or ofInputStream() instead of loading everything into memory.
- 💡 Pair with virtual threads so thousands of blocking send calls scale without async complexity.
📋 Quick Reference
| Concept | Syntax | Note |
|---|---|---|
| client | HttpClient.newHttpClient() | Reuse it; thread-safe |
| request | HttpRequest.newBuilder() | Fluent, immutable result |
| uri | .uri(URI.create(url)) | Target address |
| header | .header("Accept", "...") | Add one header |
| timeout | .timeout(Duration.ofSeconds(5)) | Per-request limit |
| GET | .GET() | No body |
| POST | .POST(BodyPublishers.ofString(s)) | Attach a body |
| send (sync) | client.send(req, handler) | Blocks for response |
| send (async) | client.sendAsync(req, handler) | CompletableFuture |
| body as text | BodyHandlers.ofString() | also ofFile / ofLines |
| status | response.statusCode() | int — check it yourself |
| header out | response.headers().firstValue("X") | Optional<String> |
📚 Resources / Keep Learning
- 📘 HttpClient — Java 21 API documentation (docs.oracle.com)
- 📘 HttpRequest — Java 21 API documentation (docs.oracle.com)
- 📗 dev.java — official tutorials and guides
🎉 Lesson Complete!
You can now build requests with the fluent builder, send them synchronously or via sendAsync, pick the right BodyHandler, set headers and timeouts, and — crucially — check the status code instead of expecting an exception.
Practice quiz
Which package holds Java's modern HTTP client?
- java.net
- java.io.http
- java.net.http
- javax.http
Answer: java.net.http. The modern client (HttpClient, HttpRequest, HttpResponse) lives in java.net.http, standardised in Java 11.
How do you create a default HttpClient?
- HttpClient.newHttpClient()
- new HttpClient()
- HttpClient.create()
- HttpClient.of()
Answer: HttpClient.newHttpClient(). HttpClient.newHttpClient() returns a client with sensible defaults; HttpClient.newBuilder() lets you configure one.
What does client.send(request, handler) do?
- Sends asynchronously
- Only builds the request
- Streams forever
- Sends synchronously and blocks for the response
Answer: Sends synchronously and blocks for the response. send(...) is the blocking call — it returns an HttpResponse once the server replies.
What does sendAsync return?
- An HttpResponse
- A CompletableFuture<HttpResponse<T>>
- void
- A String
Answer: A CompletableFuture<HttpResponse<T>>. sendAsync returns a CompletableFuture you can chain with thenApply/thenAccept and never block the calling thread.
Which BodyHandler reads the response body as text?
- BodyHandlers.ofString()
- BodyHandlers.ofText()
- BodyHandlers.asString()
- BodyHandlers.string()
Answer: BodyHandlers.ofString(). HttpResponse.BodyHandlers.ofString() collects the body into a String. There are also ofByteArray, ofFile, ofLines, etc.
How do you add a request header?
- .addHeader(...)
- .setHeader only after build
- .header("Accept", "application/json")
- .withHeader(...)
Answer: .header("Accept", "application/json"). The builder's .header(name, value) adds a header; .headers(...) adds several at once.
How do you set a per-request timeout?
- .setTimeout(5000)
- .timeout(Duration.ofSeconds(5))
- .wait(5)
- .deadline(5)
Answer: .timeout(Duration.ofSeconds(5)). The request builder takes a java.time.Duration via .timeout(...); exceeding it throws HttpTimeoutException.
Which method sends a request body (e.g. JSON)?
- .GET()
- .body(json)
- .send(json)
- .POST(BodyPublishers.ofString(json))
Answer: .POST(BodyPublishers.ofString(json)). Use .POST(HttpRequest.BodyPublishers.ofString(...)) (or .PUT) with a BodyPublisher to attach a request body.
How do you read a response header value?
- response.header("X")
- response.headers().firstValue("X")
- response.getHeader("X")
- response.h("X")
Answer: response.headers().firstValue("X"). response.headers() returns an HttpHeaders; firstValue(name) gives an Optional<String>.
What does response.statusCode() return for a successful GET?
- "OK"
- true
- 200
- 1
Answer: 200. statusCode() returns the numeric HTTP status as an int — 200 for success, 404 for not found, etc.
Continue this course
- Previous: Virtual Threads
- Next: Immutability — Build truly immutable classes with final fields, defensive copies and records
- Quick reference: Java cheat sheet
Frequently asked questions
Why use java.net.http instead of the old HttpURLConnection?
HttpURLConnection (from java.net) is clunky, verbose, and predates modern features — it has no fluent builder, no async support, and no HTTP/2. The newer java.net.http client, standardised in Java 11, gives you an immutable HttpClient, a fluent HttpRequest builder, both synchronous send and asynchronous sendAsync, HTTP/2 by default, timeouts, and pluggable body handlers. It is the right choice for any new code on Java 11+.
What is the difference between send and sendAsync?
send(request, handler) is synchronous: the calling thread blocks until the full response arrives and you get an HttpResponse back directly. sendAsync(request, handler) is asynchronous: it returns immediately with a CompletableFuture<HttpResponse<T>> that completes later, so you can fire many requests without blocking and compose the results with thenApply, thenCompose, and friends. With virtual threads, the simple blocking send often scales just fine, so you can keep code readable.
What is a BodyHandler?
A BodyHandler tells the client how to convert the incoming response body into a Java value. HttpResponse.BodyHandlers.ofString() gives you the body as a String; ofByteArray() as bytes; ofFile(path) writes it straight to disk; ofLines() streams it as a Stream<String>; and discarding() throws it away. You pass the handler as the second argument to send / sendAsync, and the response's body() returns that converted type.
How do I send a JSON request body?
Attach a BodyPublisher to a POST or PUT: HttpRequest.newBuilder().uri(...).header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(json)).build(). The HTTP client itself does not serialise objects to JSON — you produce the JSON string with a library like Jackson or Gson (or build it by hand) and hand it to ofString. On the way back, parse the response body String the same way.
How do timeouts and errors work?
Set a per-request limit with .timeout(Duration.ofSeconds(n)); if the response does not arrive in time you get an HttpTimeoutException. You can also set a connection timeout on the client builder with .connectTimeout(...). Note that an HTTP error status like 404 or 500 is NOT an exception — send returns normally and you must check response.statusCode() yourself. Only transport-level problems (no connection, timeout, DNS failure) throw.