HttpClient.newBuilder()
Configure an HttpClient's connect timeout, redirects, HTTP version, executor and cookies before building it once and sharing it.
static HttpClient.Builder newBuilder()- Added in Java Java 11
Return value
HttpClient.BuilderA new, mutable builder with every setting at its default. Call build() to create an HttpClient.
Errors
newBuilder() itself does not throw. Builder setters throw IllegalArgumentException for invalid values, such as a zero or negative connectTimeout or a priority outside 1 to 256; build() can throw UncheckedIOException if resources cannot be allocated.
Changelog
- PHP Java 19HttpClient.Builder gains localAddress(InetAddress) to bind the client's connections to a local address.
- PHP Java 21Built clients implement AutoCloseable, with close(), shutdown(), shutdownNow() and awaitTermination(Duration).
HttpClient.newBuilder() returns an HttpClient.Builder. Each setter changes the builder and returns it, and build() creates a client. Settings are fixed once the client is built; to change them, build another client.
The settings that matter for services
connectTimeout(Duration): how long to wait when a new connection has to be opened. It has no default. Set it on every client that calls another service.followRedirects(Redirect):NEVER(default),NORMAL(follow, except from HTTPS to HTTP) orALWAYS.version(Version):HTTP_2(default preference) orHTTP_1_1. Forcing HTTP/1.1 is useful when a proxy or old server misbehaves with the upgrade attempt.executor(Executor): runs the client's asynchronous tasks. Without it the client creates its own.cookieHandler(CookieHandler): pass aCookieManagerto store cookies from responses and send them back.proxy(ProxySelector),authenticator(Authenticator),sslContext(SSLContext),sslParameters(SSLParameters),priority(int)for HTTP/2 stream priority, andlocalAddress(InetAddress)(Java 19).
What is not on the client
There is no request timeout and no default header such as User-Agent or Authorization on the builder. The request timeout is per request, set with HttpRequest.Builder.timeout(). Shared headers go into a small helper that creates request builders, or into a wrapper class around the client.
Builders are not clients
The builder is mutable and not thread-safe; the client it builds is immutable and thread-safe. Build the client once at startup, often one per downstream service when they need different timeouts or TLS settings, and share it. Bad values fail at the setter, not at the first request: connectTimeout(Duration.ZERO) throws IllegalArgumentException straight away, which is what you want when the value comes from configuration.
Redirects in APIs
Following redirects is usually right for downloads and public pages. For JSON APIs, think first. A 307 or 308 keeps the method and body; a 301, 302 or 303 may turn into a GET, so a POST that gets redirected can silently lose its body. When following, response.uri() tells you where the body really came from and previousResponse() holds the redirect.
Example
When to use it
- Giving every client that calls a payment or inventory service a two-second connect timeout read from configuration.
- Forcing HTTP/1.1 for a legacy internal API behind a proxy that mishandles the HTTP/2 upgrade request.
- Following redirects when downloading files from a storage service that answers with a 302 to a signed URL.
Frequently asked questions
How do I set a timeout on Java's HttpClient?
There are two. HttpClient.newBuilder().connectTimeout(Duration) limits opening the connection and applies to the whole client. HttpRequest.newBuilder().timeout(Duration) limits how long each request waits for the response headers, and send() then throws HttpTimeoutException when it expires.
How do I make Java HttpClient follow redirects?
Build it with followRedirects(HttpClient.Redirect.NORMAL), which follows redirects except from HTTPS to HTTP, or Redirect.ALWAYS to follow every one. The default is Redirect.NEVER.
Can I set a default header on the HttpClient builder?
No. Headers belong to each HttpRequest. Create requests through a small helper that adds shared headers such as User-Agent or Authorization, or wrap the client in a class that does it for every call.
More examples
Keeping a session cookie with CookieManager
Without a cookie handler the client ignores <code>Set-Cookie</code> entirely. A <code>CookieManager</code> is shared by every request the client sends, so use a separate client per user session rather than one shared client when calling a site on behalf of different users.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// Writes a small text response; an empty body is sent with no body at all.
void reply(HttpExchange exchange, int status, String contentType, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
if (contentType != null) {
exchange.getResponseHeaders().set("Content-Type", contentType);
}
exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}
void main() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/login", exchange -> {
exchange.getResponseHeaders().add("Set-Cookie", "session=test-session-1; Path=/; HttpOnly");
reply(exchange, 204, null, "");
});
server.createContext("/account", exchange -> {
String cookie = exchange.getRequestHeaders().getFirst("Cookie");
reply(exchange, 200, "text/plain", "cookie sent: " + cookie);
});
server.start();
String base = "http://127.0.0.1:" + server.getAddress().getPort();
try (HttpClient withCookies = HttpClient.newBuilder().cookieHandler(new CookieManager()).build();
HttpClient without = HttpClient.newHttpClient()) {
for (HttpClient client : List.of(withCookies, without)) {
client.send(HttpRequest.newBuilder(URI.create(base + "/login")).build(),
HttpResponse.BodyHandlers.discarding());
IO.println(client.send(HttpRequest.newBuilder(URI.create(base + "/account")).build(),
HttpResponse.BodyHandlers.ofString()).body());
}
} finally {
server.stop(0);
}
}
// Output:
// cookie sent: session=test-session-1
// cookie sent: nullInvalid settings fail at the setter
Validating at the setter means a bad value read from configuration stops the service at startup instead of at the first outgoing call. Changing the builder after <code>build()</code> does not affect clients already built.
import java.net.http.HttpClient;
void main() {
try {
HttpClient.newBuilder().connectTimeout(Duration.ZERO);
} catch (IllegalArgumentException e) {
IO.println("connectTimeout: " + e.getMessage());
}
try {
HttpClient.newBuilder().priority(300);
} catch (IllegalArgumentException e) {
IO.println("priority: " + e.getMessage());
}
// The builder is mutable: each build() uses the settings at that moment.
HttpClient.Builder builder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1);
HttpClient first = builder.build();
HttpClient second = builder.version(HttpClient.Version.HTTP_2).build();
IO.println(first.version() + " then " + second.version());
first.close();
second.close();
}
// Output:
// connectTimeout: Invalid duration: PT0S
// priority: priority must be between 1 and 256
// HTTP_1_1 then HTTP_2Running the client's async work on virtual threads
The executor runs the client's asynchronous tasks, such as handling responses for <code>sendAsync</code>. Resources in try-with-resources close in reverse order, so the client closes before the executor it depends on; results print in list order because each future is joined in turn.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// Writes a small text response; an empty body is sent with no body at all.
void reply(HttpExchange exchange, int status, String contentType, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
if (contentType != null) {
exchange.getResponseHeaders().set("Content-Type", contentType);
}
exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}
void main() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/price/", exchange -> {
String sku = exchange.getRequestURI().getPath().substring("/price/".length());
reply(exchange, 200, "text/plain", sku + "=" + (sku.length() * 100));
});
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
String base = "http://127.0.0.1:" + server.getAddress().getPort();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
HttpClient client = HttpClient.newBuilder().executor(executor).build()) {
IO.println("custom executor: " + client.executor().isPresent());
List<CompletableFuture<String>> prices = Stream.of("a", "bb", "ccc")
.map(sku -> client.sendAsync(HttpRequest.newBuilder(URI.create(base + "/price/" + sku)).build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body))
.toList();
prices.forEach(price -> IO.println(price.join()));
} finally {
server.stop(0);
}
}
// Output:
// custom executor: true
// a=100
// bb=200
// ccc=300
Discussion