HttpResponse.statusCode() and headers()
Read the status code and response headers of an HttpResponse, with case-insensitive lookups, repeated headers and numeric values.
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 anOptional<String>with the first value.allValues(name)returns aList<String>, for headers that repeat such asSet-Cookie, or an empty list.firstValueAsLong(name)returns anOptionalLong, forContent-Lengthor aRetry-Aftergiven in seconds. It throwsNumberFormatExceptionif the value is not a number, andRetry-Aftermay also be an HTTP date.map()returns all headers as aMap<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
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>.
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.
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.
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