HTTP client
Programs often need data from the internet: exchange rates, the weather, a reply from another service. They get it over HTTP: the client sends the server a request, and the server returns a response — a status code and some text. Java has a built-in client for this, HttpClient (Java 11), from the java.net.http module. It isn't part of java.base, so even a compact file imports it: import java.net.http.*;.
A server to experiment with
So that the examples work without the internet, each of them starts a small server on your own computer. The JDK has a simple HttpServer for this. It replies with "Server got: …" and repeats the request, and for the /missing address it replies with code 404. Port 0 means "any free port" — the real number is found with server.getAddress().getPort(). The server code is in the startServer() method at the end of each example; you don't have to study it, the lesson is about the client.
A GET request
Working with the client takes three steps: create an HttpClient, build an HttpRequest with the address and send it with send. The second argument of send is a body handler: it says what to turn the response into. BodyHandlers.ofString() reads the body as a string:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String url = "http://localhost:" + server.getAddress().getPort() + "/users/1";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.statusCode());
IO.println(response.body());
server.stop(0);
}
// Practice server: replies with the request it received
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Server got: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Output:
200
Server got: GET /users/1For a real site only the address changes, for example https://api.example.com/users/1. A request is GET by default. Code 200 means "all good". There are other handlers too: ofLines() gives a stream of lines, ofFile(path) saves the response to a file, discarding() throws the body away.
Status codes and headers
A status code is a three-digit number: 2xx is success, 3xx a redirect, 4xx a client error (404 is "not found"), 5xx a server error. Besides the body, a response carries headers — "name: value" pairs with extra information. headers().firstValue(...) returns an Optional:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String base = "http://localhost:" + server.getAddress().getPort();
HttpClient client = HttpClient.newHttpClient();
for (String path : List.of("/users/1", "/missing")) {
HttpRequest request = HttpRequest.newBuilder(URI.create(base + path)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(path + " -> " + response.statusCode());
}
HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/users/1")).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.headers().firstValue("Content-Type").orElse("none"));
server.stop(0);
}
// Practice server: replies with the request it received
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Server got: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Output:
/users/1 -> 200
/missing -> 404
text/plain; charset=utf-8POST with JSON
To send data, the request is built with the POST method and a body. BodyPublishers.ofString turns a string into a request body. Data is most often sent as JSON; here it's written as a plain string, and the Content-Type header tells the server the format:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String url = "http://localhost:" + server.getAddress().getPort() + "/users";
String json = """
{"name": "Anna", "age": 28}""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.statusCode());
IO.println(response.body());
server.stop(0);
}
// Practice server: replies with the request it received
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Server got: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Output:
200
Server got: POST /users {"name": "Anna", "age": 28}timeout limits the wait for a response: if the server stays silent for more than five seconds, send throws HttpTimeoutException. PUT(...) and DELETE() work the same way. Parsing JSON from a response is easier with the Jackson or Gson libraries — the Java standard library has no JSON parser.
Asynchronous requests
send blocks the thread until the response arrives. sendAsync returns at once — a CompletableFuture in which the response will appear later. This way you can send several requests at the same time and wait for all of them:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String base = "http://localhost:" + server.getAddress().getPort();
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = new ArrayList<>();
for (String path : List.of("/news", "/weather", "/rates")) {
HttpRequest request = HttpRequest.newBuilder(URI.create(base + path)).build();
futures.add(client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body));
}
for (CompletableFuture<String> future : futures) {
IO.println(future.join());
}
server.stop(0);
}
// Practice server: replies with the request it received
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Server got: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Output:
Server got: GET /news
Server got: GET /weather
Server got: GET /ratesThe requests run in parallel, and join() collects the finished results one by one — in the order the futures sit in the list. More about CompletableFuture is in the CompletableFuture lesson. One HttpClient can and should be reused for many requests: it keeps connections open.
Common mistake
send doesn't throw an exception when the server replies with an error: 404 or 500 is a normal response too. Code that takes body() right away will mistake the error text for data:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String url = "http://localhost:" + server.getAddress().getPort() + "/missing";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
IO.println("Data: " + response.body());
} else {
IO.println("Error, code " + response.statusCode());
}
server.stop(0);
}
// Practice server: replies with the request it received
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Server got: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Output:
Error, code 404Always check statusCode() before parsing the body. An exception (IOException, for example ConnectException) happens only when there is no response at all: the server is unreachable or there's no network.