HttpResponse.BodyHandlers.ofString()

Read a whole response body as a String decoded with the server's charset, and know when to cap it or use a streaming handler instead.

Signature
static HttpResponse.BodyHandler<String> ofString()
  • Added in Java Java 11

Parameters

ParameterTypeDescription
$charsetoptionalCharset= charset from Content-Type, else UTF-8Only in the one-argument overload: the charset to decode with, ignoring the Content-Type header.

Return value

HttpResponse.BodyHandler<String>A body handler that collects the whole body in memory and decodes it with the charset named in the Content-Type header, or UTF-8 when the header names none. The overload ofString(Charset charset) always uses the given charset.

Errors

The handler itself does not throw. Bytes that are invalid in the charset are replaced with U+FFFD rather than reported; I/O failures while reading the body surface from send() as IOException. ofString(Charset) throws NullPointerException for a null charset.

A body handler decides what the bytes of a response become. HttpResponse.BodyHandlers.ofString() collects all of them and decodes them into one String, so client.send(request, BodyHandlers.ofString()) returns an HttpResponse<String> and body() is the text.

How the charset is chosen

The handler reads the charset parameter of the response's Content-Type header, as in text/plain; charset=ISO-8859-1. With no charset it uses UTF-8, which is right for JSON. Bytes that are not valid in the chosen charset do not throw: they become the replacement character U+FFFD. When a server sends Latin-1 text without saying so, the symptom is caf followed by a replacement character where café should be. If you know better than the header, ofString(charset) forces a charset.

Check the status before the body

The handler runs for every status, so a 500 gives you the error page as a string just as a 200 gives you the data. Look at statusCode(), and for JSON APIs the Content-Type, before parsing. A load balancer's HTML error page fed into a JSON parser produces an error about an unexpected < that hides the real problem.

Size

The whole body sits in memory, briefly both as bytes and as characters. That is fine for API responses of a few kilobytes and wrong for exports or downloads. Choose the handler by what you are doing with the data:

  • ofLines(): a Stream<String> read lazily, for line-delimited data such as NDJSON or CSV.
  • ofInputStream(): hand the stream to a streaming JSON parser.
  • ofFile(path): write a download straight to disk.
  • ofByteArray(): binary content such as images.
  • discarding(): only the status and headers matter.

Java 25 added BodyHandlers.limiting(handler, capacity), which wraps another handler and fails the request with an IOException once the body exceeds capacity bytes. Wrapping ofString() in it protects a service from a dependency that suddenly returns a huge body.

Choosing the type

The handler fixes the type of body(). Declaring the response as HttpResponse<String> and passing ofByteArray() does not compile, which keeps the decision explicit.

Example

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

When to use it

  • Reading a small JSON response from a configuration or feature-flag service into a String for a JSON parser.
  • Decoding text from a legacy partner system that sends ISO-8859-1 without labelling it.
  • Rejecting oversized responses from a third-party API before they exhaust the heap of a busy service.

Frequently asked questions

How do I get the response body as a String with Java HttpClient?

Pass HttpResponse.BodyHandlers.ofString() to send() or sendAsync(); the response is then an HttpResponse<String> and body() returns the decoded text.

Which charset does BodyHandlers.ofString() use?

The charset parameter of the response's Content-Type header, and UTF-8 if there is none. Use BodyHandlers.ofString(charset) to override it.

How do I handle a large response without loading it into memory?

Use BodyHandlers.ofLines() for line-based data, ofInputStream() for a streaming parser, or ofFile(path) to write it to disk. On Java 25, BodyHandlers.limiting(handler, capacity) can also cap how much any handler accepts.

More examples

The charset comes from Content-Type

The same four bytes decode three ways. Without a charset in the header the handler assumes UTF-8, where the single byte for <code>é</code> is invalid, so it becomes U+FFFD (printed here as <code>?</code>) without any exception.

Example · java
import com.sun.net.httpserver.HttpServer;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

void main() throws Exception {
    HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
    server.createContext("/", exchange -> {
        // A legacy service that always sends Latin-1 bytes.
        byte[] body = "café".getBytes(StandardCharsets.ISO_8859_1);
        String type = exchange.getRequestURI().getPath().equals("/labelled")
                ? "text/plain; charset=ISO-8859-1" : "text/plain";
        exchange.getResponseHeaders().set("Content-Type", type);
        exchange.sendResponseHeaders(200, body.length);
        try (OutputStream out = exchange.getResponseBody()) {
            out.write(body);
        }
    });
    server.start();
    String base = "http://127.0.0.1:" + server.getAddress().getPort();

    try (HttpClient client = HttpClient.newHttpClient()) {
        String labelled = client.send(HttpRequest.newBuilder(URI.create(base + "/labelled")).build(),
                HttpResponse.BodyHandlers.ofString()).body();
        String unlabelled = client.send(HttpRequest.newBuilder(URI.create(base + "/unlabelled")).build(),
                HttpResponse.BodyHandlers.ofString()).body();
        String forced = client.send(HttpRequest.newBuilder(URI.create(base + "/unlabelled")).build(),
                HttpResponse.BodyHandlers.ofString(StandardCharsets.ISO_8859_1)).body();

        IO.println("labelled:   " + labelled);
        IO.println("unlabelled: " + unlabelled.replace('\uFFFD', '?') + " (replacement char: "
                + unlabelled.contains("\uFFFD") + ")");
        IO.println("forced:     " + forced);
    } finally {
        server.stop(0);
    }
}

// Output:
// labelled:   café
// unlabelled: caf? (replacement char: true)
// forced:     café

Capping the body size (Java 25)

<code>limiting</code> wraps any handler and fails the exchange with an <code>IOException</code> once more than the capacity has arrived, so a runaway response cannot fill the heap of the calling service. The 100,003-byte body is rejected; the small one passes through untouched.

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 {
    HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
    server.createContext("/small", exchange -> reply(exchange, 200, "application/json", "{\"ok\":true}"));
    server.createContext("/huge", exchange -> reply(exchange, 200, "application/json", "[" + "0,".repeat(50_000) + "0]"));
    server.start();
    String base = "http://127.0.0.1:" + server.getAddress().getPort();

    HttpResponse.BodyHandler<String> atMost64k =
            HttpResponse.BodyHandlers.limiting(HttpResponse.BodyHandlers.ofString(), 64 * 1024);

    try (HttpClient client = HttpClient.newHttpClient()) {
        for (String path : List.of("/small", "/huge")) {
            try {
                String body = client.send(HttpRequest.newBuilder(URI.create(base + path)).build(), atMost64k).body();
                IO.println(path + ": " + body);
            } catch (IOException e) {
                IO.println(path + ": " + e.getMessage());
            }
        }
    } finally {
        server.stop(0);
    }
}

// Output:
// /small: {"ok":true}
// /huge: body exceeds capacity: 65536

Streaming lines instead of one big string

<code>ofLines()</code> hands lines to the stream as the bytes arrive, so memory use does not grow with the size of the export; close the stream so the connection is released. <code>discarding()</code> gives an <code>HttpResponse&lt;Void&gt;</code> whose body is always <code>null</code>.

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 {
    HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
    server.createContext("/events.ndjson", exchange -> {
        String body = IntStream.rangeClosed(1, 1000)
                .mapToObj(i -> "{\"id\":" + i + ",\"type\":\"" + (i % 10 == 0 ? "error" : "info") + "\"}")
                .collect(Collectors.joining("\n"));
        reply(exchange, 200, "application/x-ndjson", body);
    });
    server.start();
    URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/events.ndjson");

    try (HttpClient client = HttpClient.newHttpClient()) {
        HttpResponse<Stream<String>> response = client.send(HttpRequest.newBuilder(uri).build(),
                HttpResponse.BodyHandlers.ofLines());
        try (Stream<String> lines = response.body()) {
            long errors = lines.filter(line -> line.contains("\"type\":\"error\"")).count();
            IO.println("errors: " + errors);
        }

        HttpResponse<Void> headOnly = client.send(HttpRequest.newBuilder(uri).build(),
                HttpResponse.BodyHandlers.discarding());
        IO.println("discarding: " + headOnly.statusCode() + ", body " + headOnly.body());
    } finally {
        server.stop(0);
    }
}

// Output:
// errors: 100
// discarding: 200, body null

Discussion

  • Be the first to comment on this lesson.