17.4 Advanced HTTP Client Patterns
Master production-grade HTTP client patterns for enterprise applications.
Authentication
Multiple Authentication Methods:
import java.net.http.*;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.util.Base64;
/**
* Authentication patterns
*/
public class AuthenticationPatterns {
/**
* HTTP Basic Authentication
*/
public static HttpClient basicAuthClient(String username,
String password) {
return HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
username, password.toCharArray());
}
})
.build();
}
/**
* Manual Basic Auth header
*/
public static HttpRequest withBasicAuth(String url,
String username,
String password) {
String credentials = username + ":" + password;
String encoded = Base64.getEncoder().encodeToString(
credentials.getBytes(StandardCharsets.UTF_8));
return HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Basic " + encoded)
.GET()
.build();
}
/**
* Bearer token authentication
*/
public static HttpRequest withBearerToken(String url, String token) {
return HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer " + token)
.GET()
.build();
}
/**
* API Key authentication
*/
public static HttpRequest withAPIKey(String url, String apiKey) {
return HttpRequest.newBuilder(URI.create(url))
.header("X-API-Key", apiKey)
.GET()
.build();
}
/**
* OAuth 2.0 Bearer token
*/
public static class OAuthClient {
private final HttpClient client;
private final String tokenEndpoint;
private final String clientId;
private final String clientSecret;
private String accessToken;
private long tokenExpiresAt;
public OAuthClient(String tokenEndpoint,
String clientId,
String clientSecret) {
this.client = HttpClient.newHttpClient();
this.tokenEndpoint = tokenEndpoint;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
/**
* Get access token
*/
public String getAccessToken() throws Exception {
// Check if token is still valid
if (accessToken != null &&
System.currentTimeMillis() < tokenExpiresAt) {
return accessToken;
}
// Request new token
String credentials = clientId + ":" + clientSecret;
String encoded = Base64.getEncoder().encodeToString(
credentials.getBytes(StandardCharsets.UTF_8));
String body = "grant_type=client_credentials";
HttpRequest request = HttpRequest.newBuilder(
URI.create(tokenEndpoint))
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("Authorization", "Basic " + encoded)
.header("Content-Type",
"application/x-www-form-urlencoded")
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
// Parse response and extract token
// Simplified: real implementation would parse JSON
this.accessToken = extractToken(response.body());
this.tokenExpiresAt = System.currentTimeMillis() + 3600000;
return accessToken;
}
/**
* Create authenticated request
*/
public HttpRequest authenticatedRequest(String url)
throws Exception {
String token = getAccessToken();
return HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer " + token)
.GET()
.build();
}
private String extractToken(String response) {
// Parse JSON response to get token
return "token";
}
}
/**
* Custom authentication handler
*/
public static class APIKeyAuthenticator extends Authenticator {
private final String apiKey;
public APIKeyAuthenticator(String apiKey) {
this.apiKey = apiKey;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
// API keys don't use traditional auth mechanism
// This is for reference; usually handled via headers
return new PasswordAuthentication("api",
("key:" + apiKey).toCharArray());
}
}
}
Connection Pooling and Reuse
Optimizing Connection Usage:
/**
* Connection pooling strategies
*/
public class ConnectionPooling {
/**
* Singleton HttpClient for connection reuse
*/
public static class SharedHttpClient {
private static final HttpClient instance =
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(java.time.Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
public static HttpClient get() {
return instance;
}
}
/**
* HTTP Client with custom executor
*/
public static HttpClient createPooledClient(int poolSize) {
ExecutorService executor = Executors.newFixedThreadPool(poolSize);
return HttpClient.newBuilder()
.executor(executor)
.build();
}
/**
* Connection reuse demonstration
*/
public static void demonstrateReuse() throws Exception {
HttpClient client = HttpClient.newHttpClient();
// Multiple requests reuse connections
for (int i = 0; i < 10; i++) {
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://api.example.com/data"))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println("Request " + i + ": " +
response.statusCode());
}
System.out.println("All requests reused connection");
}
}
TLS/SSL Configuration
Secure Communication:
import javax.net.ssl.*;
/**
* TLS/SSL configuration patterns
*/
public class TLSConfiguration {
/**
* Custom SSL context
*/
public static HttpClient withCustomSSL(SSLContext sslContext) {
return HttpClient.newBuilder()
.sslContext(sslContext)
.build();
}
/**
* Disable certificate verification (unsafe - development only)
*/
public static HttpClient insecureSSL() throws Exception {
// DO NOT use in production!
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(
X509Certificate[] certs, String authType) {}
public void checkServerTrusted(
X509Certificate[] certs, String authType) {}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}, new java.security.SecureRandom());
return HttpClient.newBuilder()
.sslContext(sslContext)
.build();
}
/**
* Mutual TLS (mTLS)
*/
public static HttpClient withMutualTLS(
String keyStorePath, String keyStorePassword,
String trustStorePath, String trustStorePassword)
throws Exception {
// Load client certificate
KeyStore keyStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(keyStorePath)) {
keyStore.load(fis, keyStorePassword.toCharArray());
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keyStore, keyStorePassword.toCharArray());
// Load trusted certificates
KeyStore trustStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(trustStorePath)) {
trustStore.load(fis, trustStorePassword.toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
// Create SSL context
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(),
tmf.getTrustManagers(),
new java.security.SecureRandom());
return HttpClient.newBuilder()
.sslContext(sslContext)
.build();
}
/**
* Hostname verification
*/
public static class StrictHostnameVerifier implements
HttpsURLConnection.HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
// Verify hostname matches certificate
return hostname.equals(
session.getPeerPrincipal().getName());
}
}
}
Caching
HTTP Caching Patterns:
/**
* Caching strategies
*/
public class CachingPatterns {
/**
* Simple in-memory cache
*/
public static class SimpleCache {
private final Map<String, CachedResponse> cache =
new ConcurrentHashMap<>();
private final long maxAgeMillis;
public SimpleCache(long maxAgeMillis) {
this.maxAgeMillis = maxAgeMillis;
}
public String getOrFetch(String url, HttpClient client)
throws Exception {
CachedResponse cached = cache.get(url);
// Return if fresh
if (cached != null && !cached.isExpired()) {
return cached.content;
}
// Fetch fresh
HttpRequest request = HttpRequest.newBuilder(
URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
String content = response.body();
cache.put(url, new CachedResponse(content,
System.currentTimeMillis() + maxAgeMillis));
return content;
}
private static class CachedResponse {
String content;
long expiresAt;
CachedResponse(String content, long expiresAt) {
this.content = content;
this.expiresAt = expiresAt;
}
boolean isExpired() {
return System.currentTimeMillis() > expiresAt;
}
}
}
/**
* Respect Cache-Control headers
*/
public static class CacheControlAware {
public static boolean shouldCache(HttpResponse<String> response) {
String cacheControl = response.headers()
.firstValue("cache-control")
.orElse("");
return !cacheControl.contains("no-cache") &&
!cacheControl.contains("no-store");
}
public static long getMaxAge(HttpResponse<String> response) {
String cacheControl = response.headers()
.firstValue("cache-control")
.orElse("");
if (cacheControl.contains("max-age=")) {
int start = cacheControl.indexOf("max-age=") + 8;
int end = cacheControl.indexOf(",", start);
if (end == -1) end = cacheControl.length();
return Long.parseLong(cacheControl.substring(start, end)) * 1000;
}
return 0;
}
}
}
Request/Response Logging
Debugging and Monitoring:
/**
* Logging patterns for HTTP operations
*/
public class LoggingPatterns {
/**
* Log request details
*/
public static void logRequest(HttpRequest request) {
System.out.println("=== REQUEST ===");
System.out.println("Method: " + request.method());
System.out.println("URI: " + request.uri());
System.out.println("Headers:");
request.headers().map().forEach((name, values) -> {
System.out.println(" " + name + ": " +
String.join(", ", values));
});
}
/**
* Log response details
*/
public static void logResponse(HttpResponse<String> response) {
System.out.println("=== RESPONSE ===");
System.out.println("Status: " + response.statusCode());
System.out.println("Headers:");
response.headers().map().forEach((name, values) -> {
System.out.println(" " + name + ": " +
String.join(", ", values));
});
System.out.println("Body length: " +
response.body().length());
}
/**
* Timed request execution
*/
public static void timedRequest(HttpClient client,
HttpRequest request)
throws Exception {
long startTime = System.nanoTime();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
long duration = System.nanoTime() - startTime;
System.out.println("Request took: " +
(duration / 1_000_000) + "ms");
}
/**
* Structured logging with interceptor pattern
*/
public static class LoggingInterceptor {
public CompletableFuture<HttpResponse<String>> execute(
HttpClient client, HttpRequest request) {
logRequest(request);
long startTime = System.nanoTime();
return client.sendAsync(request,
HttpResponse.BodyHandlers.ofString())
.whenComplete((response, exception) -> {
if (exception != null) {
System.err.println("Request failed: " +
exception.getMessage());
} else {
long duration = System.nanoTime() - startTime;
System.out.println("Response status: " +
response.statusCode() +
" (took " + (duration / 1_000_000) + "ms)");
}
});
}
}
}
Production HTTP Client
Enterprise-Grade Implementation:
/**
* Production-ready HTTP client wrapper
*/
public class ProductionHTTPClient {
private final HttpClient httpClient;
private final int maxRetries;
private final java.time.Duration retryDelay;
private final Map<String, String> defaultHeaders;
private final CachingPatterns.SimpleCache cache;
public ProductionHTTPClient(int maxRetries,
java.time.Duration retryDelay) {
this.httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(java.time.Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
this.defaultHeaders = new ConcurrentHashMap<>();
this.cache = new CachingPatterns.SimpleCache(60000); // 1 minute
}
/**
* Add default header
*/
public void addDefaultHeader(String name, String value) {
defaultHeaders.put(name, value);
}
/**
* GET request with all features
*/
public CompletableFuture<String> get(String url) {
HttpRequest request = buildRequest(url, "GET", null);
return executeWithRetry(request, 0);
}
/**
* POST request with body
*/
public CompletableFuture<String> post(String url, String body) {
HttpRequest request = buildRequest(url, "POST", body);
return executeWithRetry(request, 0);
}
private HttpRequest buildRequest(String url, String method,
String body) {
HttpRequest.Builder builder = HttpRequest.newBuilder(
URI.create(url));
// Add default headers
defaultHeaders.forEach(builder::header);
// Set method and body
if ("GET".equals(method)) {
builder.GET();
} else if ("POST".equals(method) && body != null) {
builder.POST(HttpRequest.BodyPublishers.ofString(body));
builder.header("Content-Type", "application/json");
}
builder.timeout(java.time.Duration.ofSeconds(15));
return builder.build();
}
private CompletableFuture<String> executeWithRetry(
HttpRequest request, int attemptNumber) {
return httpClient.sendAsync(request,
HttpResponse.BodyHandlers.ofString())
.thenCompose(response -> {
if (response.statusCode() >= 200 &&
response.statusCode() < 300) {
return CompletableFuture.completedFuture(
response.body());
} else if ((response.statusCode() >= 500 ||
response.statusCode() == 408) &&
attemptNumber < maxRetries) {
// Retry on server error or timeout
return delayAndRetry(request, attemptNumber);
} else {
return CompletableFuture.failedFuture(
new HttpException("HTTP " +
response.statusCode()));
}
})
.exceptionally(throwable -> {
if (attemptNumber < maxRetries && isRetryable(throwable)) {
return delayAndRetry(request, attemptNumber).join();
}
throw new CompletionException(throwable);
});
}
private CompletableFuture<String> delayAndRetry(
HttpRequest request, int attemptNumber) {
return CompletableFuture.delayedExecutor(
retryDelay.toMillis(),
java.util.concurrent.TimeUnit.MILLISECONDS)
.submit(() -> null)
.thenCompose(v -> executeWithRetry(request,
attemptNumber + 1));
}
private boolean isRetryable(Throwable throwable) {
return throwable instanceof java.net.ConnectException ||
throwable instanceof HttpTimeoutException ||
throwable instanceof java.io.IOException;
}
public void close() {
// Clean up if needed
}
public static class HttpException extends RuntimeException {
public HttpException(String message) {
super(message);
}
}
}
Best Practices
1. Reuse HttpClient Instances:
private static final HttpClient client = HttpClient.newHttpClient();
2. Implement Proper Authentication:
return HttpRequest.newBuilder(uri)
.header("Authorization", "Bearer " + token)
.GET()
.build();
3. Use Timeouts at Multiple Levels:
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(10))
.GET()
.build();
4. Implement Retries with Exponential Backoff:
executeWithRetry(request, maxAttempts);
5. Configure TLS Properly:
HttpClient client = HttpClient.newBuilder()
.sslContext(customSSLContext)
.build();
6. Cache Responses When Appropriate:
cache.getOrFetch(url, client);
7. Log Request and Response Details:
logRequest(request);
logResponse(response);
These advanced patterns enable building robust, secure, and efficient HTTP clients for production Java applications.