HttpClient.send()

Send a request and block until the response arrives, and handle the exceptions it throws and the error statuses it does not.

Signature
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException
  • Added in Java Java 11

Parameters

ParameterTypeDescription
$requestHttpRequestThe request to send.
$responseBodyHandlerHttpResponse.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 of IOException) when the request's timeout() elapses before the response headers arrive. Its message is request timed out.
  • ConnectException when nothing is listening on the port, and HttpConnectTimeoutException (a subclass of HttpTimeoutException) when the client's connect timeout elapses.
  • Other IOExceptions for resets, protocol errors or a closed client.
  • InterruptedException when 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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: ConnectException

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

Example · java
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: true

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

Example · java
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

  • Be the first to comment on this lesson.