The Modern HttpClient
The Modern HttpClient
Before Java 11, making an HTTP request from Java code required either the cumbersome HttpURLConnection API (verbose, blocking, and full of gotchas) or a third-party library such as Apache HttpClient or OkHttp. Java 11 shipped java.net.http — a modern, standards-aligned HTTP client built into the JDK. It supports HTTP/1.1 and HTTP/2, WebSockets, fully non-blocking I/O through CompletableFuture, and a clean fluent builder API.
This lesson covers the two fundamental pieces of the API: building an HttpClient and constructing an HttpRequest. Sending requests and handling responses are the focus of the next lesson.
The Core Design: Immutable Value Objects Built with Builders
The entire API centres on two immutable types:
HttpClient— a thread-safe, reusable HTTP engine. Create one per application (or per logical concern) and reuse it for all requests. Heavy construction cost is paid once.HttpRequest— an immutable description of a single request (URI, method, headers, body). Build a new one for each call.
Both are constructed with nested Builder inner classes following the standard Java builder pattern you already know from the Streams and Collections APIs. Once built, instances are immutable and therefore safe to share across threads.
HttpClient carries connection pools, SSL context, and executor state. Making it immutable eliminates accidental shared-state bugs in concurrent code — you configure it once and trust it never changes.
Creating an HttpClient
The simplest possible client uses all defaults:
Defaults give you HTTP/2 with HTTP/1.1 fallback, the common JVM SSL context, and a daemon thread pool. That is enough for many applications. When you need to customise behaviour, reach for the builder:
Key HttpClient Configuration Options
- version() —
Version.HTTP_2(default) orVersion.HTTP_1_1. HTTP/2 multiplexes multiple requests over a single TCP connection, giving lower latency at scale. The client negotiates via ALPN and falls back gracefully. - followRedirects() —
NEVER,ALWAYS, orNORMAL(follows all redirects except HTTPS to HTTP downgrade).NORMALis the safe production choice. - connectTimeout() — how long to wait for the initial TCP connection. Prevents threads from hanging indefinitely against a firewall-dropped endpoint. Note: this is connection timeout only; read/write timeouts are set per-request.
- executor() — the thread pool for async operations. On Java 21+ passing
Executors.newVirtualThreadPerTaskExecutor()lets you fire thousands of concurrent HTTP calls cheaply without tuning a platform thread pool. - sslContext() — supply a custom
SSLContextwhen you need mutual TLS, a custom trust store, or self-signed certificates in a test environment. - authenticator() — an
Authenticatorfor HTTP Basic/Digest challenges (rare in modern APIs; most use bearer tokens in headers instead). - cookieHandler() — plug in a
CookieManagerfor session-cookie workflows. Default: no cookies stored. - proxy() — route traffic through a corporate proxy:
ProxySelector.of(new InetSocketAddress("proxy.corp.com", 8080)).
static final field if needed.
Building an HttpRequest
An HttpRequest bundles everything that describes a single HTTP call: the target URI, the HTTP method, headers, an optional body publisher, and an optional per-request timeout. Build one with HttpRequest.newBuilder():
Setting timeout() on the request means: if the server does not complete the response within that window the client throws an HttpTimeoutException. This is distinct from the connect timeout on the client — you typically want both.
HTTP Methods and Body Publishers
Use the method-specific builder methods. GET and DELETE carry no body. POST, PUT, and PATCH carry a body expressed as an HttpRequest.BodyPublisher:
The most useful built-in BodyPublisher factories in BodyPublishers:
ofString(String)— UTF-8 string body. The most common choice for JSON payloads.ofFile(Path)— streams a file directly from disk with no heap buffering. Use this for large uploads.ofByteArray(byte[])— raw bytes. Useful when you already serialized the payload.noBody()— explicitly marks a request body as absent. Equivalent to omitting a body publisher.
Setting Multiple Headers and Overriding Headers
The builder provides both header(name, value) (which adds a header, supporting multi-value headers) and headers(name, value, name, value, ...) for bulk setting. To replace a previously set value use setHeader(name, value):
Host, Content-Length, or Connection manually. The JDK computes and enforces them. Attempting to set them throws an IllegalArgumentException at request-build time. Check the HttpRequest Javadoc for the full restricted list.
Reusing a Request Template with copy()
Because HttpRequest is immutable, you cannot modify one after building it. However, you can copy a builder from an existing request and override specific fields:
The copy constructor HttpRequest.newBuilder(existingRequest, headerFilter) clones all settings; the filter lambda selects which headers to carry over (true keeps all).
Putting It Together: A Minimal Service Pattern
This pattern separates the description of a request from the sending of it, which makes the builder methods trivially unit-testable without any network calls.
Summary
HttpClient is a reusable, thread-safe engine — create it once and share it. HttpRequest is an immutable per-call description — build a new one each time. Configure the client with version, redirect policy, connect timeout, and an executor. Configure the request with URI, method, headers, body publisher, and per-request timeout. Use BodyPublishers to supply bodies from strings, files, or byte arrays. With both objects in hand, the next step is sending the request and processing the response.