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.