17.1 HTTP Client Fundamentals
Master the modern Java HTTP Client for building efficient web applications.
Core Concepts
HTTP/1.1 vs HTTP/2:
import java.net.http.*;
import java.net.URI;
import java.time.Duration;
/**
* Understand HTTP protocol versions
*/
public class HTTPVersions {
/**
* HTTP/1.1 client (default in legacy code)
*/
public static HttpClient createHTTP1Client() {
return HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
}
/**
* HTTP/2 client (preferred for modern applications)
*/
public static HttpClient createHTTP2Client() {
return HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
}
/**
* HTTP/2 with fallback to HTTP/1.1
*/
public static HttpClient createHTTP2WithFallback() {
return HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
// Falls back to HTTP/1.1 if server doesn't support HTTP/2
.build();
}
/**
* HTTP version comparison
*/
public static void demonstrateVersions() {
System.out.println("HTTP/1.1 characteristics:");
System.out.println("- One request per connection");
System.out.println("- Suitable for simple requests");
System.out.println("- Lower complexity");
System.out.println("\nHTTP/2 characteristics:");
System.out.println("- Multiplexing (multiple requests per connection)");
System.out.println("- Server push support");
System.out.println("- Better performance for many requests");
System.out.println("- Header compression");
}
}
HttpClient Builder
Configuration Options:
/**
* Configure HttpClient with various options
*/
public class HttpClientConfiguration {
/**
* Minimal client with defaults
*/
public static HttpClient minimalClient() {
return HttpClient.newHttpClient();
}
/**
* Custom configuration
*/
public static HttpClient customClient() {
return HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
"username", "password".toCharArray());
}
})
.build();
}
/**
* Redirect policies
*/
public static HttpClient createClientWithRedirectPolicy(
HttpClient.Redirect policy) {
return HttpClient.newBuilder()
.followRedirects(policy)
.build();
}
/**
* Executor (advanced)
*/
public static HttpClient clientWithExecutor(ExecutorService executor) {
return HttpClient.newBuilder()
.executor(executor)
.build();
}
/**
* SSL/TLS context
*/
public static HttpClient clientWithSSL(SSLContext sslContext) {
return HttpClient.newBuilder()
.sslContext(sslContext)
.build();
}
/**
* Proxy configuration
*/
public static HttpClient clientWithProxy(String proxyHost, int proxyPort) {
ProxySelector proxySelector = ProxySelector.of(
new InetSocketAddress(proxyHost, proxyPort));
return HttpClient.newBuilder()
.proxy(proxySelector)
.build();
}
/**
* Cookie jar (advanced)
*/
public static HttpClient clientWithCookies(CookieManager cookieManager) {
return HttpClient.newBuilder()
.cookieHandler(cookieManager)
.build();
}
}
Redirect Policy
Understanding Redirect Handling:
/**
* Different redirect policies
*/
public class RedirectPolicies {
/**
* NEVER - don't follow redirects
*/
public static HttpClient neverFollowRedirects() {
return HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.build();
}
/**
* ALWAYS - follow all redirects
*/
public static HttpClient alwaysFollowRedirects() {
return HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
}
/**
* NORMAL - follow redirects on same protocol/host
*/
public static HttpClient normalFollowRedirects() {
return HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}
/**
* Demonstrate redirect behavior
*/
public static void demonstrateRedirects() throws Exception {
HttpClient client = normalFollowRedirects();
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://example.com/old-url"))
.GET()
.build();
// Will automatically follow redirect to new URL
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println("Final URL: " + response.uri());
System.out.println("Status: " + response.statusCode());
}
}
Basic Request-Response
Simple HTTP GET:
/**
* Basic HTTP operations
*/
public class BasicHTTPOperations {
/**
* Simple GET request
*/
public static String simpleGet(String url) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}
/**
* GET with custom headers
*/
public static String getWithHeaders(String url,
Map<String, String> headers)
throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url));
// Add headers
for (Map.Entry<String, String> header : headers.entrySet()) {
builder.header(header.getKey(), header.getValue());
}
HttpRequest request = builder.GET().build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}
/**
* GET with timeout
*/
public static String getWithTimeout(String url, int timeoutSeconds)
throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(timeoutSeconds))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}
/**
* Example usage
*/
public static void main(String[] args) throws Exception {
// Simple GET
String result = simpleGet("https://api.example.com/data");
System.out.println("Response: " + result);
// With headers
Map<String, String> headers = Map.of(
"Accept", "application/json",
"User-Agent", "MyApp/1.0"
);
String jsonResponse = getWithHeaders(
"https://api.example.com/items",
headers);
System.out.println("JSON: " + jsonResponse);
// With timeout
String timeoutResponse = getWithTimeout(
"https://api.example.com/slow",
10);
System.out.println("Slow response: " + timeoutResponse);
}
}
Response Inspection
Working with Response Metadata:
/**
* Inspect response details
*/
public class ResponseInspection {
/**
* Check status and headers
*/
public static void inspectResponse(String url) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
// Status code
int status = response.statusCode();
System.out.println("Status: " + status);
// Headers
HttpHeaders headers = response.headers();
headers.map().forEach((name, values) -> {
System.out.println(name + ": " + String.join(", ", values));
});
// URI (after redirects)
System.out.println("Final URI: " + response.uri());
// Body
String body = response.body();
System.out.println("Body length: " + body.length());
}
/**
* Status code classification
*/
public static boolean isSuccessful(int statusCode) {
return statusCode >= 200 && statusCode < 300;
}
public static boolean isClientError(int statusCode) {
return statusCode >= 400 && statusCode < 500;
}
public static boolean isServerError(int statusCode) {
return statusCode >= 500 && statusCode < 600;
}
/**
* Check content type
*/
public static String getContentType(HttpResponse<String> response) {
return response.headers()
.firstValue("content-type")
.orElse("text/plain");
}
/**
* Get content length
*/
public static long getContentLength(HttpResponse<String> response) {
return response.headers()
.firstValueAsLong("content-length")
.orElse(-1L);
}
}
Connection Management
Reusing HttpClient:
/**
* HTTP client best practices
*/
public class ConnectionManagement {
/**
* Shared client instance (recommended)
*/
public static class APIClient {
private static final HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
/**
* Reuse client for multiple requests
*/
public String fetchData(String url) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}
}
/**
* Connection pooling benefits
*/
public static void demonstratePooling() {
HttpClient client = HttpClient.newHttpClient();
System.out.println("Connection Pooling Benefits:");
System.out.println("1. Connection reuse across requests");
System.out.println("2. Reduced latency (no new connection per request)");
System.out.println("3. Automatic SSL session reuse");
System.out.println("4. Keep-Alive support");
}
}
Timeouts
Timeout Configuration:
/**
* Timeout patterns
*/
public class TimeoutPatterns {
/**
* Client-level timeout
*/
public static HttpClient clientLevelTimeout(int seconds) {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(seconds))
.build();
}
/**
* Request-level timeout
*/
public static String requestWithTimeout(String url, int timeoutSeconds)
throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(timeoutSeconds))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}
/**
* Both levels (request overrides client)
*/
public static String withBothTimeouts(String url) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5)) // Connection timeout
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(10)) // Request timeout
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
}
/**
* Timeout exception handling
*/
public static String getWithFallback(String url, String fallbackValue) {
try {
return requestWithTimeout(url, 5);
} catch (HttpTimeoutException e) {
System.err.println("Request timed out: " + e.getMessage());
return fallbackValue;
} catch (Exception e) {
System.err.println("Request failed: " + e.getMessage());
return fallbackValue;
}
}
}
Best Practices
1. Reuse HttpClient:
// ✓ Good - shared instance
private static final HttpClient client = HttpClient.newHttpClient();
// ✗ Bad - creating new client for each request
HttpClient client = HttpClient.newHttpClient();
2. Configure Appropriate Timeouts:
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(10))
.GET()
.build();
3. Use HTTP/2:
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
4. Handle Redirects Explicitly:
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
5. Proper Resource Management:
try (HttpClient client = HttpClient.newHttpClient()) {
// Use client
} // Closed automatically
These fundamentals provide the foundation for building robust HTTP clients in modern Java applications.