HttpResponse.statusCode() and headers()

Read the status code and response headers of an HttpResponse, with case-insensitive lookups, repeated headers and numeric values.

Signature
int statusCode()
HttpHeaders headers()
  • Added in Java Java 11

Return value

int / HttpHeadersstatusCode() returns the HTTP status code, such as 200 or 404. headers() returns the response headers as an immutable HttpHeaders with case-insensitive names.

Errors

Neither method throws. On the returned HttpHeaders, firstValueAsLong(name) throws NumberFormatException when the value is not a number.

Every HttpResponse carries the status code and headers alongside the body. statusCode() returns an int; headers() returns an HttpHeaders, an immutable view of the header names and their values.

Status codes

The client never turns a status into an exception, so this number is how your code learns that a call failed. Test the class of the status (code / 100 == 2) when any success will do, and specific codes when they mean different things to you: 404 as an empty result, 409 as a conflict to report, 429 and 503 as signals to back off and retry, other 5xx as dependency failures. Logging the status of every outgoing call is one of the cheapest debugging aids a service can have.

Reading headers

  • firstValue(name) returns an Optional<String> with the first value.
  • allValues(name) returns a List<String>, for headers that repeat such as Set-Cookie, or an empty list.
  • firstValueAsLong(name) returns an OptionalLong, for Content-Length or a Retry-After given in seconds. It throws NumberFormatException if the value is not a number, and Retry-After may also be an HTTP date.
  • map() returns all headers as a Map<String, List<String>>.

Header names are case-insensitive in every one of these, including map().get(...). Over HTTP/1.1 the client stores names in lower case, so code that prints map() sees content-type whatever the server wrote. HTTP/2 always sends names in lower case anyway.

Mistakes to avoid

Do not compare firstValue("Content-Type") with equals("application/json"): servers often add ; charset=utf-8. Use startsWith or parse the media type. Do not assume a header is present; Optional is there to make you decide what happens without it.

Other parts of the response

uri() is where the response came from, which differs from the request's URI after a redirect. previousResponse() holds the redirect response, version() the protocol actually used, and request() the request that was finally sent.

Example

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

When to use it

  • Backing off for the number of seconds a rate-limited API sends in Retry-After before retrying.
  • Logging the downstream status code and X-Request-Id of every outgoing call so failures can be traced across services.
  • Rejecting a response whose Content-Type is not JSON before handing the body to a parser.

Frequently asked questions

How do I get the status code from Java HttpClient?

Call statusCode() on the HttpResponse that send() returns, or inside a thenApply stage after sendAsync(). It returns an int such as 200, 404 or 503.

How do I read a response header with Java HttpClient?

Use response.headers().firstValue("Header-Name"), which returns an Optional<String>; the name is case-insensitive. Use allValues(name) for headers that can repeat, such as Set-Cookie.

Why are the header names in HttpHeaders.map() lower case?

Header names are case-insensitive in HTTP, and the client stores HTTP/1.1 names in lower case; HTTP/2 sends them in lower case anyway. Lookups with firstValue, allValues and map().get ignore case, so the original spelling does not matter.

More examples

Case-insensitive names and repeated headers

Every lookup ignores case, including <code>map().get</code>, and the client stored the HTTP/1.1 names in lower case. <code>firstValue</code> would silently drop the second cookie, which is why repeated headers need <code>allValues</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("/login", exchange -> {
        exchange.getResponseHeaders().add("Set-Cookie", "session=test-1; HttpOnly");
        exchange.getResponseHeaders().add("Set-Cookie", "theme=dark");
        exchange.getResponseHeaders().add("X-RateLimit-Remaining", "99");
        reply(exchange, 200, "text/plain", "welcome");
    });
    server.start();
    URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/login");

    try (HttpClient client = HttpClient.newHttpClient()) {
        HttpResponse<String> response = client.send(HttpRequest.newBuilder(uri).build(),
                HttpResponse.BodyHandlers.ofString());
        var headers = response.headers();
        IO.println("first cookie: " + headers.firstValue("SET-COOKIE").orElseThrow());
        IO.println("all cookies:  " + headers.allValues("set-cookie"));
        IO.println("remaining:    " + headers.firstValueAsLong("x-ratelimit-remaining").orElse(-1));
        IO.println("via map():    " + headers.map().get("Content-Type"));
        IO.println("absent:       " + headers.allValues("ETag") + " " + headers.firstValue("ETag"));

        List<String> names = new ArrayList<>(headers.map().keySet());
        names.remove("date");   // the Date value changes on every run
        IO.println("names:        " + names);
    } finally {
        server.stop(0);
    }
}

// Output:
// first cookie: session=test-1; HttpOnly
// all cookies:  [session=test-1; HttpOnly, theme=dark]
// remaining:    99
// via map():    [text/plain]
// absent:       [] Optional.empty
// names:        [content-length, content-type, set-cookie, x-ratelimit-remaining]

Numeric headers and HttpHeaders in tests

<code>firstValueAsLong</code> parses with <code>Long.parseLong</code> semantics, so an HTTP-date throws. Real <code>Retry-After</code> headers can be either form, so production code should try the number and fall back to parsing a date.

Example · java
import java.net.http.HttpHeaders;

void main() {
    // HttpHeaders.of builds headers without a server, handy in unit tests.
    HttpHeaders headers = HttpHeaders.of(
            Map.of("Retry-After", List.of("120"), "X-Retry-At", List.of("Wed, 21 Oct 2026 07:28:00 GMT")),
            (name, value) -> true);

    IO.println("retry-after: " + headers.firstValueAsLong("retry-after").orElse(0));
    try {
        headers.firstValueAsLong("X-Retry-At");
    } catch (NumberFormatException e) {
        IO.println("not a number: " + e.getMessage());
    }

    // The filter drops headers you do not want, for example before logging.
    HttpHeaders safe = HttpHeaders.of(
            Map.of("Authorization", List.of("Bearer test-token"), "Accept", List.of("application/json")),
            (name, value) -> !name.equalsIgnoreCase("Authorization"));
    IO.println("filtered: " + safe.map());
}

// Output:
// retry-after: 120
// not a number: For input string: "Wed, 21 Oct 2026 07:28:00 GMT"
// filtered: {Accept=[application/json]}

Where the response really came from

After a followed redirect, <code>statusCode()</code> and <code>headers()</code> belong to the final response. The redirect itself is still available through <code>previousResponse()</code>, with its own status and headers, which is how you find out which file "latest" pointed to.

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("/files/latest", exchange -> {
        exchange.getResponseHeaders().set("Location", "/files/report-2026-10.csv");
        reply(exchange, 302, null, "");
    });
    server.createContext("/files/report-2026-10.csv", exchange -> reply(exchange, 200, "text/csv", "id,total\n1,10"));
    server.start();
    URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/files/latest");

    try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build()) {
        HttpResponse<String> response = client.send(HttpRequest.newBuilder(uri).build(),
                HttpResponse.BodyHandlers.ofString());
        IO.println("status:   " + response.statusCode() + " " + response.headers().firstValue("content-type").orElse(""));
        IO.println("asked:    " + uri.getPath());
        IO.println("got:      " + response.uri().getPath());
        response.previousResponse().ifPresent(previous -> IO.println("redirect: " + previous.statusCode()
                + " to " + previous.headers().firstValue("location").orElse("?")));
    } finally {
        server.stop(0);
    }
}

// Output:
// status:   200 text/csv
// asked:    /files/latest
// got:      /files/report-2026-10.csv
// redirect: 302 to /files/report-2026-10.csv

Discussion

  • Be the first to comment on this lesson.