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

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

Pro Tips

📋 Quick Reference

ConceptSyntaxNote
clientHttpClient.newHttpClient()Reuse it; thread-safe
requestHttpRequest.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 textBodyHandlers.ofString()also ofFile / ofLines
statusresponse.statusCode()int — check it yourself
header outresponse.headers().firstValue("X")Optional<String>

📚 Resources / Keep Learning

🎉 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

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.

Related lessons