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.