HttpClient.send()
Send a request and block until the response arrives, and handle the exceptions it throws and the error statuses it does not.
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException- Added in Java Java 11
Parameters
| Parameter | Type | Description |
|---|---|---|
$request | HttpRequest | The request to send. |
$responseBodyHandler | HttpResponse.BodyHandler<T> | Decides what the response body becomes, for example BodyHandlers.ofString() or BodyHandlers.discarding(). |
Return value
HttpResponse<T>The response, with its status code, headers and a body of type T produced by the body handler. It returns for every HTTP status, including 4xx and 5xx.
Errors
IOException when sending or receiving fails, for example a refused connection, a closed client or a reset; its subclass HttpTimeoutException when the request timeout elapses before the response headers arrive. InterruptedException if the calling thread is interrupted while waiting. IllegalArgumentException if the request is not valid for this client.
client.send(request, bodyHandler) sends the request and blocks the calling thread until the status, headers and body have been received and handled. The body handler's type sets the response type: BodyHandlers.ofString() gives an HttpResponse<String>.
Statuses are not exceptions
send returns normally for a 404, a 409 or a 503. Only failures to get a response at all throw. That is the opposite of what many HTTP libraries do, and code that wraps send in a try and assumes success inside it will happily parse an error page as data. Check statusCode() first, then decide: map a 404 to an empty Optional, a 409 to a domain error, and a 5xx to an exception your retry or circuit-breaking logic understands.
What it throws
HttpTimeoutException(a subclass ofIOException) when the request'stimeout()elapses before the response headers arrive. Its message isrequest timed out.ConnectExceptionwhen nothing is listening on the port, andHttpConnectTimeoutException(a subclass ofHttpTimeoutException) when the client's connect timeout elapses.- Other
IOExceptions for resets, protocol errors or a closed client. InterruptedExceptionwhen the waiting thread is interrupted, for example when an executor shuts down.
Catch HttpTimeoutException before IOException if you treat timeouts differently, for example by answering 504 rather than 502.
Interruption
The checked InterruptedException tempts people into catch (Exception e). The interrupted flag is already cleared when the exception is thrown, so swallowing it means whoever asked the thread to stop never finds out. If you cannot rethrow it, call Thread.currentThread().interrupt() before wrapping it.
Blocking is fine on virtual threads
A blocked platform thread costs memory and a slot in a pool, which is why older code reached for sendAsync. On a virtual thread the blocking send is cheap: the carrier thread is released while the request waits. In a server that runs handlers on virtual threads, plain send with ordinary try/catch is usually the clearest code, and calling several services at once is a matter of submitting each call to a virtual-thread executor.
Without a request timeout send can wait indefinitely. Set one on every request to another service.
Example
When to use it
- Looking up a customer in a CRM service from a request handler and returning 404 when the CRM does too.
- Answering 504 Gateway Timeout when a pricing service exceeds its two-second request timeout.
- Building a dashboard response from three backend services called in parallel on virtual threads.
Frequently asked questions
Does Java HttpClient throw an exception for 404 or 500?
No. send() returns the HttpResponse for every status code. It throws IOException only when no response could be obtained, such as a refused connection or a timeout, so check statusCode() yourself.
How do I catch a timeout in Java HttpClient?
Set timeout(Duration) on the HttpRequest and catch java.net.http.HttpTimeoutException around send(). It extends IOException, so catch it before any IOException handler.
Should I use send() or sendAsync()?
On virtual threads, or anywhere blocking a thread is acceptable, send() gives simpler code and normal exceptions. Use sendAsync() when you are already composing CompletableFuture pipelines or must not block the calling thread.
More examples
Timeouts and refused connections
The catch for <code>HttpTimeoutException</code> comes first because it is the more specific type. A refused connection fails fast with <code>ConnectException</code>; it is the stalled server, not the absent one, that needs the timeout.
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
void main() throws Exception {
CountDownLatch release = new CountDownLatch(1);
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/slow", exchange -> {
try {
release.await(); // answers only after the client has given up
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
exchange.sendResponseHeaders(204, -1);
exchange.close();
});
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
String base = "http://127.0.0.1:" + server.getAddress().getPort();
try (HttpClient client = HttpClient.newHttpClient()) {
HttpRequest slow = HttpRequest.newBuilder(URI.create(base + "/slow"))
.timeout(Duration.ofMillis(300))
.build();
try {
client.send(slow, HttpResponse.BodyHandlers.discarding());
} catch (HttpTimeoutException e) {
IO.println("timeout: " + e.getMessage() + " (an IOException: " + (e instanceof IOException) + ")");
}
release.countDown();
server.stop(0);
// Nothing listens on the port any more.
try {
client.send(HttpRequest.newBuilder(URI.create(base + "/slow")).build(),
HttpResponse.BodyHandlers.discarding());
} catch (IOException e) {
IO.println("no server: " + e.getClass().getSimpleName());
}
}
}
// Output:
// timeout: request timed out (an IOException: true)
// no server: ConnectExceptionKeeping the interrupt when you catch it
Throwing <code>InterruptedException</code> clears the flag, as the first line shows. Restoring it lets loops and executors further up see that the thread was asked to stop.
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String fetch(HttpClient client, URI uri) {
try {
return client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()).body();
} catch (InterruptedException e) {
IO.println("flag after catch: " + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt(); // put the flag back for the caller
throw new IllegalStateException("interrupted while calling " + uri.getPath(), e);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
void main() throws Exception {
CountDownLatch arrived = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/report", exchange -> {
arrived.countDown();
try {
release.await(); // never answers before the caller is interrupted
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
exchange.sendResponseHeaders(204, -1);
exchange.close();
});
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/report");
try (HttpClient client = HttpClient.newHttpClient()) {
Thread worker = Thread.ofVirtual().start(() -> {
try {
fetch(client, uri);
} catch (IllegalStateException e) {
IO.println(e.getMessage());
IO.println("flag restored: " + Thread.currentThread().isInterrupted());
}
});
arrived.await(); // the request is in flight
worker.interrupt(); // for example, the request that needed the report was cancelled
worker.join();
} finally {
release.countDown();
server.stop(0);
}
}
// Output:
// flag after catch: false
// interrupted while calling /report
// flag restored: trueCalling three services at once with virtual threads
Each blocking <code>send</code> runs on its own virtual thread, so all three requests are in flight at once; the server holds each one until the other two arrive, which a one-at-a-time loop could never satisfy. The results print in submission order because each <code>Future</code> is read in turn.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// Writes a small text response; an empty body is sent with no body at all.
void reply(HttpExchange exchange, int status, String contentType, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
if (contentType != null) {
exchange.getResponseHeaders().set("Content-Type", contentType);
}
exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}
void main() throws Exception {
CountDownLatch inFlight = new CountDownLatch(3);
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/", exchange -> {
// Each call waits here until all three have arrived, which only
// happens if the client sends them at the same time.
inFlight.countDown();
boolean together = false;
try {
together = inFlight.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (!together) {
reply(exchange, 504, "text/plain", "not parallel");
return;
}
reply(exchange, 200, "text/plain", exchange.getRequestURI().getPath().substring(1) + " ok");
});
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
String base = "http://127.0.0.1:" + server.getAddress().getPort();
try (HttpClient client = HttpClient.newHttpClient();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> calls = Stream.of("profile", "orders", "recommendations")
.map(name -> executor.submit(() -> client.send(
HttpRequest.newBuilder(URI.create(base + "/" + name)).timeout(Duration.ofSeconds(8)).build(),
HttpResponse.BodyHandlers.ofString()).body()))
.toList();
for (Future<String> call : calls) {
IO.println(call.get());
}
IO.println("calls completed: " + calls.size());
} finally {
server.stop(0);
}
}
// Output:
// profile ok
// orders ok
// recommendations ok
// calls completed: 3
Discussion