Status Codes and Headers
Pick the status code a client can act on, set and read headers correctly on both sides, and avoid the case and multi-value traps.
exchange.getResponseHeaders().set("Location", "/orders/101");
exchange.getResponseHeaders().add("Set-Cookie", "a=1");
exchange.sendResponseHeaders(201, body.length); // 204: pass -1
response.headers().firstValue("location");The status code is the first thing a client, load balancer or monitoring system looks at, often the only thing. Choose it so that a program can decide what to do next without reading your body.
The codes APIs actually use
| Code | Use it when |
|---|---|
| 200 / 201 / 204 | Success; 201 for a created resource with a Location header; 204 for success with no body, such as DELETE. |
| 400 / 422 | The request is malformed, or well formed but fails validation. |
| 401 / 403 | 401: not authenticated (missing or bad credentials). 403: authenticated but not allowed. |
| 404 / 405 / 409 | No such resource; method not supported (send Allow); conflicts with current state, such as a duplicate email. |
| 413 / 415 / 429 | Body too large; wrong Content-Type; rate limited (send Retry-After). |
| 500 / 502 / 503 / 504 | Your bug; a bad upstream answer; temporarily unavailable; an upstream timed out. |
Two habits cause real damage. Returning 200 with {"error": ...} in the body makes every client, retry policy and dashboard believe the call worked. Returning 500 for bad input makes monitoring page someone for a client's mistake. 4xx means "the caller should change something"; 5xx means "the server failed, retrying later may help".
Headers on the server
exchange.getResponseHeaders() returns a Headers object: a case-insensitive map from name to a list of values. set replaces all values; add appends one, which is what you want for headers that may repeat, such as Set-Cookie. The JDK server normalises names to its own form (Content-type, Set-cookie), which is harmless because HTTP header names are case-insensitive. Set every header before sendResponseHeaders; afterwards they are ignored.
A 204 or 304 has no body by definition, so pass -1 as the length.
Headers on the client
HttpResponse.headers() gives an HttpHeaders with case-insensitive lookups: firstValue(name) returns an Optional, allValues(name) a list, and firstValueAsLong parses numbers. On requests, HttpClient refuses to let you set restricted headers that it manages itself, such as Host, Connection, Content-Length, Expect and Upgrade, and header() throws IllegalArgumentException if you try.
Example
When to use it
- Answer POST /orders with 201 and a Location header the client can follow to the new resource.
- Signal rate limiting with 429 and Retry-After so partner integrations back off instead of hammering the API.
- Set a session cookie and a preference cookie on the same response without one overwriting the other.
More examples
set versus add, and case-insensitive names
Using <code>set</code> for <code>Set-Cookie</code> would silently drop the session cookie when a second cookie is written. The stored names show the server's normalised spelling, which is why header lookups must always go through these case-insensitive methods.
import com.sun.net.httpserver.Headers;
void main() {
Headers headers = new Headers();
headers.set("Cache-Control", "no-store");
headers.set("cache-control", "max-age=60"); // replaces: same header, different case
headers.add("Set-Cookie", "session=abc123; HttpOnly");
headers.add("SET-COOKIE", "theme=dark"); // appends a second value
IO.println("Cache-Control -> " + headers.get("CACHE-CONTROL"));
IO.println("Set-Cookie -> " + headers.get("set-cookie"));
IO.println("first cookie -> " + headers.getFirst("Set-Cookie"));
IO.println("stored names -> " + new TreeSet<>(headers.keySet()));
}
// Output:
// Cache-Control -> [max-age=60]
// Set-Cookie -> [session=abc123; HttpOnly, theme=dark]
// first cookie -> session=abc123; HttpOnly
// stored names -> [Cache-control, Set-cookie]429 with Retry-After
A 429 tells the caller it did nothing wrong except go too fast, and <code>Retry-After</code> tells it how long to back off; without it clients tend to retry at once and make the overload worse. <code>Retry-After</code> may also be an HTTP date, which makes <code>firstValueAsLong</code> throw <code>NumberFormatException</code>, so a production client should handle both forms.
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 {
AtomicInteger used = new AtomicInteger();
int limit = 2;
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
server.createContext("/quote", exchange -> {
if (used.incrementAndGet() > limit) {
exchange.getResponseHeaders().set("Retry-After", "30"); // seconds
exchange.sendResponseHeaders(429, -1);
exchange.close();
return;
}
byte[] body = "{\"price\":42}".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(body);
}
});
server.start();
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/quote");
try (HttpClient client = HttpClient.newHttpClient()) {
for (int i = 1; i <= 3; i++) {
HttpResponse<String> r = client.send(HttpRequest.newBuilder(uri).build(),
HttpResponse.BodyHandlers.ofString());
if (r.statusCode() == 429) {
long wait = r.headers().firstValueAsLong("retry-after").orElse(1);
IO.println("call " + i + ": 429, retry in " + wait + "s");
} else {
IO.println("call " + i + ": " + r.statusCode() + " " + r.body());
}
}
} finally {
server.stop(0);
}
}
// Output:
// call 1: 200 {"price":42}
// call 2: 200 {"price":42}
// call 3: 429, retry in 30sRestricted request headers
The client computes these from the URI, the body publisher and its own connection handling, so letting you override them could produce an invalid request. Code ported from an older client that set <code>Host</code> or <code>Content-Length</code> by hand fails here, as soon as <code>header()</code> is called, rather than on the wire.
import java.net.http.HttpRequest;
void main() {
URI uri = URI.create("http://127.0.0.1:8080/");
for (String name : List.of("Accept", "Authorization", "Host", "Content-Length", "Connection")) {
try {
HttpRequest.newBuilder(uri).header(name, "x").build();
IO.println(name + ": allowed");
} catch (IllegalArgumentException e) {
IO.println(name + ": " + e.getMessage());
}
}
}
// Output:
// Accept: allowed
// Authorization: allowed
// Host: restricted header name: "Host"
// Content-Length: restricted header name: "Content-Length"
// Connection: restricted header name: "Connection"
Discussion