The catalog no longer lives in the same program
The library catalog may be managed by a remote service. While books were in a local Map, looking up a code meant executing a method in the same process. Across the network, new possibilities appear: the server may not respond, the response may arrive slowly, an intermediary may return an error and the received text may not have the expected format. The chapter's question is how to obtain data without confusing “no book” with “I could not ask”.
A host is a node reachable through a name or address; DNS can translate a name into a network address. A port distinguishes services on the node. TCP provides an ordered byte stream between two endpoints; UDP transports datagrams and does not by itself promise delivery, order or absence of duplicates. HTTP is an application protocol above a transport: it defines requests, responses, methods, statuses and headers. A response body may be text, image bytes or a format such as JSON. Figure 27.1 separates these levels, because a DNS error, TCP timeout, HTTP 404 code and malformed JSON require different diagnoses.
A URI identifies a resource with a scheme and other parts, for example https://example.invalid/books/42. A URL is a form of reference that also indicates an access mechanism, but in the modern client we start from the URI representation. Do not construct arbitrary components by concatenating user-supplied strings: spaces, ?, #, slashes and non-ASCII characters have meanings or encoding rules. A query must be built by encoding parameter values according to the service contract, without indiscriminately encoding the entire URI. Resolving a relative reference with URI.resolve is a semantic operation, rather than simple textual juxtaposition.
For example, the book key Java & Networks cannot be added without preparation to ...?title=. The & character would separate parameters, changing the question sent to the server. Conversely, encoding an entire URI as a single value would also transform its delimiters. First determine which component must receive the data, then apply that component's encoding rules. The final textual representation can be printed in a test, while avoiding logging queries containing credentials or personal data.
A first socket and its limit
A TCP Socket connects the process to a host and port and provides input and output streams, which we learned to manage in chapter 25. If we invent a line-based protocol, we must establish where a request ends, which charset is used, how an error and a value are distinguished and what happens if the peer closes midway. The socket does not interpret HTTP for us. DatagramSocket for UDP suits other protocols, but its lack of connection does not automatically mean “faster for any service”. For an HTTP catalog we will use the API that already expresses HTTP requests and responses.
Client, request, response
HttpClient is the configured object that can serve several requests and reuse resources such as connections. An HttpRequest describes a specific request: URI, method, headers, possible body and timeout. HttpResponse<T> contains status, headers and a body of type T determined by the chosen BodyHandler. Choosing BodyHandlers.ofString(StandardCharsets.UTF_8) declares that the body should be read as UTF-8 text; ofByteArray retains the bytes, ofFile writes to a file and ofInputStream leaves the caller a stream to consume or close. These forms do not change the meaning of the HTTP code.
GET requests a representation of the resource; a request with a body uses a BodyPublisher, for example BodyPublishers.ofString when the application protocol accepts text. The Content-Type header describes the format sent or received, but does not automatically validate the body. If the server declares JSON and sends invalid text, transport may have worked perfectly while application interpretation fails. The client should preserve in diagnostics at least a secret-free URI, method, status and parsing cause; indiscriminately logging the entire body may instead expose confidential data or saturate logs.
In the complete program, we start a small server only on the machine's loopback address, request /books and print response and body. The teaching server uses jdk.httpserver, a JDK module distinct from the general Java SE specification and not a production-server framework. Compile with javac --release 25 --add-modules jdk.httpserver LocalClient.java and run with java --add-modules jdk.httpserver LocalClient. Port 0 lets the system choose a free port: there is no need to fix a port that might be occupied. The client is closed at the end of the try, the server in the finally.
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class LocalClient {
public static void main(String[] args) throws IOException, InterruptedException {
HttpServer server = HttpServer.create(
new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0), 0);
server.createContext("/books", exchange -> {
byte[] body = "book=Java".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(200, body.length);
try (var output = exchange.getResponseBody()) {
output.write(body);
}
});
server.start();
try (HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2)).build()) {
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort()
+ "/books");
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(3)).GET().build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("HTTP " + response.statusCode());
System.out.println(response.body());
} finally {
server.stop(0);
}
}
}
The expected output is HTTP 200 followed by book=Java. The body is not read directly from the socket stream: the BodyHandler converts it into the requested type. If the service responded 404, send would still return an HTTP response to examine; a status is not itself an IOException. If instead the hostname does not resolve or the connection fails, there is no HTTP response to interpret. This distinction prevents turning every failure into Optional.empty() as if the book simply did not exist.
Key concept – A status is a protocol fact. The server may respond
200with an unexpected body,404for an absent resource,429for a request limit or503for temporary unavailability. The program must read the service contract and decide which statuses are normal results and which require error handling.BodyHandlers.ofStringdoes not automatically reject bodies of unsuccessful statuses.
HttpClient configuration includes a preference for HTTP/1.1 or HTTP/2, redirects, proxies and authentication. Configure what the system needs; a version preference does not guarantee the peer will use exactly that version. The connection timeout concerns the phase establishing the connection, while HttpRequest.timeout limits the response operation's duration according to the API contract. These are different limits. A ten-second timeout does not prove the server did not process a request: the client may lose the response after the server has already done the work.
Waiting, errors and retries
send waits for the result and may throw IOException or InterruptedException. If the thread is interrupted, the caller must respect its context's interruption policy; a method catching the exception without being able to rethrow it should at least restore the flag. sendAsync returns a CompletableFuture<HttpResponse<T>>: it allows asynchronous work to be composed and exposes failure through exceptional completion. It does not turn networking into an immediate operation or relieve us of correctly consuming a streaming body. In chapter 20 we saw that a virtual thread allows maintaining linear blocking code while many requests wait for I/O; we must still limit concurrent requests, memory and the remote service's capacity. Figure 27.2 shows that the decision to retry comes after classifying the failure.
Chapter 20A showed that a local CAS can make only one reservation succeed in JVM memory. If the reservation becomes an HTTP request to a remote archive, that CAS does not control the server's state. After a timeout, the client may not know whether the server completed the operation: retrying without an idempotency contract or agreed request key may duplicate it. The guarantee must be built into the protocol and the archive receiving the request.
Repeating a request may be useful after a temporary error, but it is not a general cure. An idempotent operation produces the same intended effect if repeated under the same conditions; this does not mean every attempt has identical logs or timings. A GET querying data is often repeatable under the HTTP contract, while a POST creating an invoice may duplicate an effect if the client did not receive the first response. Even when a method is theoretically idempotent, the service's concrete implementation matters. A retry has a maximum attempt count, controlled waits, possibly jitter to avoid synchronizing clients, and a criterion for statuses and errors. It must be observable; an infinite loop hiding the error increases load precisely when the service is struggling.
By jitter we mean a small random variation in the wait between attempts. If a thousand clients all wait exactly one second after a 503, they may return together and overload the server again; distributing waits reduces this concentration. The variation must respect a limit and does not replace a maximum attempt count. If the server communicates a waiting time with a header such as Retry-After, the HTTP and service contracts guide the decision. An automatic retry must also be cancelable when the caller is no longer interested in the result.
Canceling a CompletableFuture may interrupt ongoing work in a way dependent on the implementation and the operation's stage. It does not mean “the server did not execute”. A remote modification requires a protocol allowing attempts to be correlated, for example an idempotency key when supported by the service. Timeout handling must preserve this uncertainty, rather than declare that the modification failed without evidence.
HTTPS and peer identity
HTTPS uses TLS to protect transport and verify peer identity according to trust configuration. An untrusted certificate is a signal to diagnose, rather than an invitation to disable all checks. The application must know where trust material comes from and how credentials are updated. An Authenticator and a CookieHandler may be part of client configuration; more complex identity protocols need a specific contract. Passwords must not be placed in URIs, logs or distributed examples.
If the server presents a certificate valid for a name different from the requested host, the problem concerns neither body parsing nor the HTTP code: the client does not yet have a trustworthy application response. A system using its own certificate authority must distribute trust through a controlled process; accepting any certificate makes identity checking pointless. User authentication is a subsequent, different question: an HTTPS channel can be protected even when the request does not yet contain application credentials.
Body format and size
The body is a second boundary. Our server returns simple text book=Java; if it returned JSON, HttpClient would deliver strings or bytes, rather than an already validated domain object. Java SE does not mandate a general JSON parser. A declared library, grammar and field validation are needed. Before writing a response to disk, also define a size limit: ofString and ofByteArray bring the body into memory. For large content, choose a file or stream handler, handling closure and publication as in chapter 25.
ofFile is convenient when the body can be written directly into the chosen destination. If the file represents an archive read by other processes, convenience does not replace controlled publication: download into a temporary file, validate its size and content, then decide whether to replace the visible file. This is the same brick from chapter 25 applied to a remote source. If the response is consumed with ofInputStream, closing the stream is the caller's responsibility; leaving it open may prevent resource release and delay orderly client closure.
When continuous messages are needed
WebSocket maintains an exchange of messages in both directions after a specific opening. A listener receives events and must respect requests for subsequent messages and consumer pressure; it is not a differently named variant of HttpClient.send. It suits problems requiring continuous updates, such as real-time catalog state, and entails an application protocol for connection, reconnection and closure.
If the screen asks only for a book title when the user presses “Search”, an ordinary HTTP request has a natural beginning and end. If instead the screen must receive a stream of catalog changes, a WebSocket may avoid repeated queries; however, we must define what happens after disconnection. Will the server send a complete snapshot, or events starting from the sequence number after the last received? Without this rule, reconnection can show an inconsistent catalog even when the transport channel works.
Transferring the solution to the real service
The local server guarantees a controlled positive case. First modify it to respond 404, then 503 once and finally 200. The client must distinguish “book absent” from “service temporarily unavailable” and retry only if the contract allows repeating the request. Record how many calls were made; a test looking only at the final body does not see an excessive retry loop. A second experiment delays the response beyond the timeout and verifies which information the client really possesses about possible server work.
Finally, ask ten virtual threads to query the service with a limit of three simultaneous requests. The point is not to demonstrate that ten is a magic number: it is to show that the client's concurrency model and the peer's capacity are different limits. Readers have transferred the brick when they can choose between send, sendAsync and a virtual thread for their program, and explain which errors remain possible in each model.
Essential references. The Java 25
HttpClientspecification describes reuse, resources and closure; theBodyHandlersspecification clarifies that predefined handlers do not examine status. Retry policy and body form depend on the service contract, rather than the client API.