Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/1

For 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-8

POST 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 /rates

The 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 404

Always 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.

Thanks for reading!

Donate to the site — we work hard for you.