The Java 11 HTTP Client finally ships in the JDK and it changes how I write network code.
Remember the years of HttpURLConnection gymnastics and third party clients glued on top of it. Apache HttpClient and OkHttp saved many of us, but the standard library felt stuck. JEP 321 landed and with it came java.net.http with HTTP 2 and WebSocket support, a clean builder API, and simple bodies. The first time I read a request built with this API I felt I was reading English. Fewer nested try blocks, no mysterious streams dance, and a clear model for sync and async work. Here is the smallest GET I would ship to production without a helper class.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class Fetch {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}Readable, right. A single builder per piece, predictable defaults, and a body handler that makes intent obvious. I like that timeouts and redirects are first class. No need to wrap streams or chase InputStream close calls. If you are still on Java 8 you can still compile against this by switching toolchains or sticking to the old client for a while, but if you have JDK 11, just use it.
Async is where the client shines for me, since CompletableFuture keeps the call site tidy without callbacks nesting all over the place. Here is a simple async call.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
.header("Accept", "application/json")
.build();
CompletableFuture<String> body =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
body.thenAccept(b -> System.out.println("bytes " + b.length()))
.exceptionally(ex -> { ex.printStackTrace(); return null; })
.join();That future can be combined, raced, or throttled with an Executor, and the client will honor back pressure with streaming bodies. If you enable HTTP 2 on the server side, this client will multiplex over one connection with no drama. For more advanced cases you can pass a BodySubscriber and process bytes as they arrive.
Configuration is simple and explicit. You can set a proxy, a cookie manager, an authenticator, a custom SSL context, or just trust the defaults. The builder lets you pick the preferred protocol version and the redirect policy. Here is a quick sketch I use when I need a tuned client for a scraper or a command line job.
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.proxy(ProxySelector.getDefault())
.cookieHandler(new java.net.CookieManager())
.authenticator(Authenticator.getDefault())
.connectTimeout(Duration.ofSeconds(10))
.build();One more gift in this package is a WebSocket client. It is not a wrapper around the old HTTP classes. It is a separate, modern API that plays well with reactive flows. You can connect, send text or binary, and implement back pressure with a few lines.
HttpClient client = HttpClient.newHttpClient();
WebSocket ws = client.newWebSocketBuilder()
.buildAsync(URI.create("wss://echo.websocket.org"), new WebSocket.Listener() {
@Override public void onOpen(WebSocket webSocket) {
webSocket.sendText("hi", true);
webSocket.request(1);
}
@Override public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
System.out.println("got " + data);
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "bye");
return CompletableFuture.completedFuture(null);
}
}).join();Should you replace Apache HttpClient or OkHttp tomorrow. Not a hard rule. Those are great and battle tested, and they still hold features you may need. For teams who want one less dependency, the Java 11 HTTP Client gives you HTTP 2, WebSocket, redirects, cookies, proxy, TLS, and a fluent Request builder right in the JDK. If you need interceptors, custom connection pools, or special retry logic, keep your current client. If what you want is a clean standard API plus great defaults, this one is ready.
Sending data is just as clear. BodyPublishers cover strings, byte arrays, files, and input streams. You can also create your own publisher for chunked uploads. Here is a tiny POST with JSON, plus a file upload sketch.
HttpRequest postJson = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"hello\":\"world\"}"))
.build();
HttpRequest postFile = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.POST(HttpRequest.BodyPublishers.ofFile(Paths.get("data.bin")))
.build();Testing is straight forward if you add a thin wrapper. The class is concrete, so I create a tiny interface around the calls I use, inject it in services, and plug a fake in tests. For integration tests I point requests to WireMock or a local server and assert on status, headers, and body. Since the API is small the seam stays small and the test code reads like a narrative.
Some quick notes after a week of use. The default client has no cookie storage. If you need cookies, set a cookie handler. The default redirect policy is normal, which follows most redirects except a few security sensitive ones. You can override it on the builder or the request. Timeouts matter, set both connect and per request values. The body handlers that collect to a string or byte array will keep data in memory, so pick a streaming handler for large downloads. Expect continue exists on the request builder and it helps when you send large bodies to picky servers.
Performance wise, the client takes advantage of HTTP 2 features like header compression and multiplexing. With TLS the JDK can negotiate ALPN and pick HTTP 2 when the server supports it. You do not have to change the request code. On a dev laptop I see fewer sockets and fewer context switches when I fetch many small items from the same host. The async path scales well since you can keep a small thread pool and still move many requests in parallel. If your service needs very high throughput, measure with your traffic and your servers, but the signs are good.
Bottom line, this is a standard tool that makes HTTP code readable, testable, and friendly to async work.
Grab JDK 11, switch a small service, and see the difference today.
Less code, fewer surprises, faster feedback, happier team.