HTTP for Java Developers
Read a request and a response the way the server sees them, and know which Java APIs handle each side of the conversation.
METHOD /path?query HTTP/1.1
Header-Name: value
[body]
HTTP/1.1 STATUS Reason
Header-Name: value
[body]Every web API you build or call in Java speaks the same text protocol underneath. A client sends a request: a method, a target path with an optional query string, headers, and sometimes a body. The server answers with a status code, headers and an optional body. Frameworks hide this, but the bugs you will chase in production (a missing Content-Type, a 301 your client never followed, a body read twice) all live at this level.
GET /v1/orders/42?expand=items HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 25
{"id":42,"status":"paid"}
Methods carry promises
GET and HEAD are safe: they must not change anything on the server. GET, HEAD, PUT and DELETE are idempotent: sending the same request twice leaves the server in the same state as sending it once. POST and PATCH make no such promise. This matters the moment you add retries: a client, proxy or load balancer may safely resend an idempotent request after a timeout, but a blindly retried POST /payments can charge a card twice.
Headers and bodies
Header names are case-insensitive, so never compare them with equals. HTTP/2 sends them in lower case, and the JDK's server rewrites them to its own capitalisation. A body's length is announced either with Content-Length (in bytes, not characters) or by chunked transfer encoding when the size is not known up front. The fragment part of a URL (#top) never reaches the server at all.
The Java pieces
com.sun.net.httpserver(modulejdk.httpserver): a small HTTP server in the JDK. Despite the package name it is a supported, documented API.java.net.http.HttpClient(modulejava.net.http, Java 11): the standard client, with HTTP/1.1, HTTP/2, synchronous and asynchronous calls.java.net.URI,URLEncoderandURLDecoderfor building and taking apart addresses.
Production services usually run on a servlet container such as Tomcat or Jetty, or on a framework like Spring Boot that embeds one. The rest of this stage uses the JDK classes directly, so you can see exactly what those layers do for you.
Example
When to use it
- Debug a client integration by reproducing the exact request line and headers a partner API expects.
- Decide which outbound calls a retry interceptor may repeat after a timeout and which need an idempotency key.
- Build service URLs from configuration and know which parts the server will actually receive.
More examples
Taking a URL apart with java.net.URI
The server only ever receives the path and query; the fragment stays in the browser. Code that builds a base URL from <code>getPort()</code> must handle -1, which means "the scheme's default".
void main() {
URI uri = URI.create("https://api.example.com:8443/v1/orders/42?status=paid&page=2#items");
IO.println("scheme: " + uri.getScheme());
IO.println("host: " + uri.getHost());
IO.println("port: " + uri.getPort());
IO.println("path: " + uri.getPath());
IO.println("query: " + uri.getQuery());
IO.println("fragment: " + uri.getFragment());
// Without an explicit port, getPort() returns -1, not 443.
IO.println("default: " + URI.create("https://api.example.com/").getPort());
}
// Output:
// scheme: https
// host: api.example.com
// port: 8443
// path: /v1/orders/42
// query: status=paid&page=2
// fragment: items
// default: -1Which methods are safe to retry
Retry policies in API clients and gateways are built on exactly this table. A POST that timed out may already have succeeded, so retrying it needs an idempotency key rather than a plain resend.
enum Method {
GET(true, true), HEAD(true, true), PUT(false, true),
DELETE(false, true), POST(false, false), PATCH(false, false);
final boolean safe;
final boolean idempotent;
Method(boolean safe, boolean idempotent) {
this.safe = safe;
this.idempotent = idempotent;
}
}
boolean mayRetryAfterTimeout(Method m) {
// A timeout means "unknown outcome": only repeat requests that cannot double-apply.
return m.idempotent;
}
void main() {
for (Method m : Method.values()) {
IO.println(m + " safe=" + m.safe + " idempotent=" + m.idempotent
+ " retry=" + mayRetryAfterTimeout(m));
}
}
// Output:
// GET safe=true idempotent=true retry=true
// HEAD safe=true idempotent=true retry=true
// PUT safe=false idempotent=true retry=true
// DELETE safe=false idempotent=true retry=true
// POST safe=false idempotent=false retry=false
// PATCH safe=false idempotent=false retry=falseWhat the JDK client actually sends
The client defaults to HTTP/2, so on a plain <code>http://</code> URL it asks to upgrade with <code>Upgrade</code> and <code>HTTP2-Settings</code> headers; the JDK server ignores that and answers in HTTP/1.1. Note how the server reports header names with its own capitalisation, such as <code>User-agent</code>.
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 -> {
// Echo the request line and the header names the server received.
String echo = exchange.getRequestMethod() + " " + exchange.getRequestURI() + "\n"
+ new TreeSet<>(exchange.getRequestHeaders().keySet());
byte[] body = echo.getBytes(StandardCharsets.UTF_8);
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() + "/v1/ping?verbose=1");
try (HttpClient client = HttpClient.newHttpClient()) {
HttpResponse<String> response = client.send(
HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
IO.println(response.body());
IO.println("answered with " + response.version());
} finally {
server.stop(0);
}
}
// Output:
// GET /v1/ping?verbose=1
// [Connection, Host, Http2-settings, Upgrade, User-agent]
// answered with HTTP_1_1
Discussion