How do I use Java 11 HttpClient in a multithreaded environment?

When using the Java 11 HttpClient in a multithreaded environment, you need to ensure it is used in a thread-safe and efficient manner. Thankfully, the HttpClient introduced in Java 11 is designed to be thread-safe and can be shared among multiple threads without additional synchronization.

Here are the important points about using HttpClient in a multithreaded environment:


1. Create a Single HttpClient Instance

  • HttpClient is thread-safe, so create a single shared instance of HttpClient and reuse it across your application. Do not create a new instance of HttpClient for every request, as it can lead to performance inefficiencies.
  • Use the factory method HttpClient.newHttpClient() to create a default instance, or configure it with a custom builder.

Example:

package org.kodejava.net.http;

import java.net.http.HttpClient;
import java.time.Duration;

public class HttpClientBuilder {
    // Create a shared instance of HttpClient
    private static final HttpClient httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2) // Support HTTP/2
            .connectTimeout(Duration.ofSeconds(10)) // Set up a timeout
            .build();

    public static HttpClient getHttpClient() {
        return httpClient;
    }
}

2. Use Thread-Safe Methods

  • Use the methods send() (for synchronous requests) or sendAsync() (for asynchronous requests) provided by HttpClient. These methods are thread-safe and can be invoked concurrently by multiple threads.
  • You should handle response callbacks correctly in the case of asynchronous usage since they will be executed by the internal thread pool.

Synchronous Request Example:

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public class SynchronousRequest {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts"))
                .GET()
                .build();

        // Using the shared HttpClient instance
        HttpResponse<String> response = HttpClientBuilder.getHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Response code: " + response.statusCode());
        System.out.println("Response body: " + response.body());
    }
}

Asynchronous Request Example:

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.concurrent.CompletableFuture;

public class AsynchronousRequest {
    public static void main(String[] args) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
                .GET()
                .build();

        // Asynchronous call
        CompletableFuture<HttpResponse<String>> futureResponse =
                HttpClientBuilder.getHttpClient()
                        .sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Handle response when it is ready
        futureResponse.thenAccept(response -> {
            System.out.println("Response code: " + response.statusCode());
            System.out.println("Response body: " + response.body());
        }).join(); // Wait for the response to complete
    }
}

3. Internal Thread Pool

  • By default, the HttpClient uses an internal thread pool managed by a ForkJoinPool for asynchronous requests. You can customize this by providing your own Executor when configuring the HttpClient with the builder.

Example: Custom Executor

package org.kodejava.net.http;

import java.net.http.HttpClient;
import java.util.concurrent.Executors;

public class CustomExecutorExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClient.newBuilder()
                .executor(Executors.newFixedThreadPool(10))
                .build();

        // Use this HttpClient instance for requests
    }
}

4. Timeouts

  • Set a connection timeout at the HttpClient level using the connectTimeout method.
  • For individual requests, you can specify a request timeout.

Example:

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;

public class RequestWithTimeout {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts"))
                .timeout(Duration.ofSeconds(5)) // Set request-specific timeout
                .GET()
                .build();

        HttpResponse<String> response = HttpClientBuilder.getHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println("Response body: " + response.body());
    }
}

5. Handle Exceptions

  • Handle exceptions like timeouts, ConnectException, or server errors gracefully when making HTTP calls.
  • Use try-catch blocks for synchronous methods and handle() or exceptionally() for asynchronous methods.

Example:

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.concurrent.CompletableFuture;

public class HandleExceptionsExample {
    public static void main(String[] args) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://invalid.url"))
                .GET()
                .build();

        CompletableFuture<HttpResponse<String>> futureResponse = HttpClientBuilder.getHttpClient()
                .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .exceptionally(e -> {
                    System.out.println("Request failed: " + e.getMessage());
                    return null;
                });

        futureResponse.thenAccept(response -> {
            if (response != null) {
                System.out.println("Response code: " + response.statusCode());
            }
        }).join();
    }
}

6. Handling Concurrency

  • In multithreaded scenarios, the HttpClient can process concurrent requests efficiently since it is non-blocking, especially when using sendAsync(), and does not require manual synchronization.
  • You can run multiple requests in parallel by delegating each request to a thread or submitting tasks to an ExecutorService.

Example: Multiple Requests in Parallel

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class ParallelRequestsExample {
    public static void main(String[] args) {
        List<URI> uris = List.of(
                URI.create("https://jsonplaceholder.typicode.com/posts/1"),
                URI.create("https://jsonplaceholder.typicode.com/posts/2"),
                URI.create("https://jsonplaceholder.typicode.com/posts/3")
        );

        List<CompletableFuture<Void>> futures = uris.stream()
                .map(uri -> {
                    HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
                    return HttpClientBuilder.getHttpClient()
                            .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                            .thenAccept(response -> {
                                System.out.println("Response for " + uri + " : " + response.body());
                            });
                })
                .toList();

        // Wait for all requests to complete
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
    }
}

Key Best Practices

  1. Reuse a single HttpClient instance for better resource management.
  2. Use asynchronous methods (sendAsync) for non-blocking and parallel requests.
  3. Configure timeouts to avoid hanging.
  4. Customize the thread pool if your application has specific concurrency requirements.
  5. Properly handle errors and exceptions in both synchronous and asynchronous requests.

By following these guidelines, you can effectively use HttpClient in a multithreaded environment.

How do I monitor performance and latency of Java 11 HTTP requests?

Monitoring the performance and latency of Java 11 HTTP requests is essential when utilizing the java.net.http package introduced with the new Java 11 HttpClient API. It helps identify bottlenecks, optimize network calls, and ensure efficient resource usage for your application.

Here’s how you can do it:

1. Measure Latency Using Timestamps

You can manually measure the time taken to send and receive HTTP requests by recording timestamps before and after executing the HTTP call.

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;

public class HttpPerformanceMonitor {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://example.com"))
                .GET()
                .build();

        // Record start time
        Instant start = Instant.now();

        // Send request and get response
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // Record end time
        Instant end = Instant.now();

        // Calculate latency
        Duration latency = Duration.between(start, end);
        System.out.println("Response time: " + latency.toMillis() + " ms");
        System.out.println("Response status code: " + response.statusCode());
    }
}

This approach calculates the time taken for the entire HTTP operation, including connection establishment, sending the request, and receiving the response.

2. Use a Custom Executor to Monitor Thread Usage

HttpClient allows you to set a custom Executor for handling its asynchronous operations. You can measure how efficiently the threads are being utilized.

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class HttpClientWithCustomExecutor {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(4); // Monitor thread usage

        HttpClient client = HttpClient.newBuilder()
                .executor(executor)
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .GET()
                .build();

        long startTime = System.currentTimeMillis();

        client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenAccept(response -> {
                    long endTime = System.currentTimeMillis();
                    System.out.println("Response time: " + (endTime - startTime) + " ms");
                    System.out.println("Response status code: " + response.statusCode());
                })
                .join(); // Wait for the async call to complete

        executor.shutdown();
    }
}

By setting a custom Executor, you can track thread pool utilization and capture latency.

3. Enable Logging for HTTP Headers and Debugging

Java 11 supports configuring logging for the HTTP Client. You can enable debug-level logging to capture low-level details:

  • Add the following JVM options to enable java.net.http.HttpClient debugging:
-Djdk.httpclient.HttpClient.log=requests,headers,frames:all

This will output detailed log information, including HTTP request/response headers and frames.

4. Monitor HTTP Client Metrics Using Libraries

Libraries that support metrics collection (like Micrometer) can be integrated with Java 11 HttpClient to collect throughput, response times, and errors.
Example: Using Micrometer You can create custom timers and counters to record metrics manually.

package org.kodejava.net.http;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;

public class HttpClientWithMetrics {
    public static void main(String[] args) throws Exception {
        MeterRegistry registry = new SimpleMeterRegistry(); // Replace with a distributed registry like Prometheus

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://example.com"))
                .GET()
                .build();

        registry.timer("http.requests").record(() -> {
            try {
                Instant start = Instant.now();
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                Instant end = Instant.now();

                System.out.println("Response time: " + Duration.between(start, end).toMillis() + " ms");
                System.out.println("Response status code: " + response.statusCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        registry.get("http.requests").timers().forEach(timer -> {
            System.out.println("Timer count: " + timer.count());
            System.out.println("Total time: " + timer.totalTime(TimeUnit.MILLISECONDS) + " ms");
        });
    }
}

5. Use Dependency Injection Frameworks

Combine the Java 11 HttpClient with Spring Boot’s actuator (if you’re already using it). Actuator provides built-in metrics and HTTP tracing capabilities.

6. Profiling with External Tools

You can observe Java application performance (including HTTP requests) using APM tools, such as:

  • Java Flight Recorder (JFR)
    • Use JFR to monitor detailed HTTP request timings and underlying JVM performance.
  • Java VisualVM
    • Profile application threads and network usage.
  • Third-Party APMs
    • Tools like New Relic, AppDynamics, or Datadog can report on HTTP client usage.

Summary of Approaches

Approach Tools/Techniques Notes
Manual Timing Instant + Duration Tracks latency at the request level.
Custom Executors ExecutorService Measures thread usage and async execution.
Logging JVM Debugging (HttpClient.log) Debugs detailed network activity.
Metrics Libraries Micrometer (manual instrumentation) Can feed metrics into observability tools.
Framework Integration Spring Boot Actuator Collects trace and performance metrics.
External Profiling/Monitoring JFR, APM Tools, VisualVM Monitors JVM, HTTP, and app performance.

Choose the approach based on your application’s complexity and monitoring needs!


Maven Dependencies

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
    <version>1.15.0</version>
</dependency>

Maven Central

How do I retry failed requests using Java 11 HttpClient?

Retrying failed requests using Java 11’s HttpClient involves implementing a custom retry mechanism. Java 11’s HttpClient doesn’t provide native support for automatic retries, but you can build a retry mechanism into your application by wrapping the logic in a loop.

Here’s how you can do it step by step:

1. Define a Retry Mechanism

You’ll need to decide on:

  • The maximum number of retry attempts.
  • An optional delay between retries.
  • The type of failures that should trigger retries (e.g., IOException).

2. Create a Function to Retry Requests

You can encapsulate the retry logic in a utility method or by using a loop.

3. Full Code Example:

Below is an example implementation:

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.io.IOException;

public class HttpClientWithRetry {

   public static void main(String[] args) {
      HttpClient httpClient = HttpClient.newHttpClient();
      HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://api.example.com/resource"))
              .timeout(Duration.ofSeconds(10))
              .GET()
              .build();

      int maxRetries = 3;  // Max number of retries
      Duration delay = Duration.ofSeconds(2); // Delay between retries

      try {
         HttpResponse<String> response = sendWithRetries(httpClient, request, maxRetries, delay);
         System.out.println("Response received: " + response.body());
      } catch (Exception e) {
         System.err.println("Request failed after retries: " + e.getMessage());
      }
   }

   public static HttpResponse<String> sendWithRetries(HttpClient client,
                                                      HttpRequest request,
                                                      int maxRetries,
                                                      Duration delay) throws Exception {
      int attempts = 0;

      while (true) {
         try {
            // Attempt to send the request
            return client.send(request, HttpResponse.BodyHandlers.ofString());
         } catch (IOException | InterruptedException ex) {
            attempts++;
            if (attempts > maxRetries) {
               throw new Exception("Maximum retry attempts reached.", ex);
            }
            System.out.println("Request failed, retrying... (" + attempts + "/" + maxRetries + ")");
            Thread.sleep(delay.toMillis()); // Delay before retry
         }
      }
   }
}

Explanation:

  1. HttpClient Initialization:
    • The HttpClient is created using HttpClient.newHttpClient().
  2. Retry Logic:
    • The sendWithRetries method sends the HTTP request and retries on exceptions (IOException or InterruptedException in this example).
    • A loop tracks the number of attempts and retries until either the request succeeds or the maximum retry limit is reached.
  3. Delay Between Retries:
    • A Thread.sleep introduces a delay (defined by a Duration object) between retries.
  4. Failure Handling:
    • If all retries fail, an exception is thrown indicating the request could not be completed.
  5. Configurations:
    • You can adjust the retry count, delay, and consider making the logic more robust by checking specific HTTP status codes for retries (e.g., 5xx server errors).

Future Improvements:

  • Exponential Backoff: Increase the delay between retries exponentially to ease server load.
  • Timeout Handling: Handle custom timeout scenarios with detailed configuration.
  • Customizable Errors: Customize retry logic based on specific HTTP response codes (e.g., retry only for 500–599 responses).

How do I handle errors and exceptions in Java 11 HttpClient?

In Java 11, the HttpClient API provides a flexible way to handle HTTP requests and responses. When dealing with errors and exceptions, there are several strategies and patterns you can use to ensure your application can handle unexpected issues effectively while making HTTP calls.

1. Exception Types in Java 11 HttpClient

Here are some common exceptions that may occur while using the HttpClient API:

  • java.net.ConnectException: Thrown when a connection cannot be established.
  • java.net.SocketTimeoutException: Thrown if a request times out.
  • java.net.UnknownHostException: Thrown when the host cannot be resolved.
  • java.io.IOException: A general I/O error, such as a failed read/write operation.
  • java.net.http.HttpTimeoutException: Specific to HTTP operations when a request or response timeout is reached.

2. Handling Exceptions with try-catch

You can surround your HTTP client logic with a try-catch block to gracefully handle exceptions. Here is an example:

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.UnknownHostException;

public class HttpClientErrorHandlingExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .GET()
                .build();

        try {
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            // Handle successful responses
            if (response.statusCode() == 200) {
                System.out.println("Response received: " + response.body());
            } else {
                System.out.println("Non-OK response: " + response.statusCode());
                System.out.println("Response body: " + response.body());
            }

        } catch (HttpTimeoutException e) {
            System.err.println("Request timed out: " + e.getMessage());
        } catch (UnknownHostException e) {
            System.err.println("Unknown Host: " + e.getMessage());
        } catch (ConnectException e) {
            System.err.println("Connection error: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("I/O error occurred: " + e.getMessage());
        } catch (InterruptedException e) {
            System.err.println("Request interrupted: " + e.getMessage());
            Thread.currentThread().interrupt(); // Restore interrupted status
        }
    }
}

Explanation

  1. try-catch to Isolate Block: Specific exceptions such as HttpTimeoutException or ConnectException are caught and handled to provide more detailed information about what went wrong.
  2. Default IOException: This covers any I/O-related issue not explicitly handled by other exceptions.
  3. Logging Errors: In this case, errors are printed to System.err, but in a production system, you should use a proper logging framework (e.g., SLF4J or Log4j).

3. Timeouts: Configure Proper Timeouts for Error Prevention

Handling timeouts properly can prevent your client from hanging indefinitely due to network issues.

HttpClient httpClient = HttpClient.newBuilder()
        .connectTimeout(java.time.Duration.ofSeconds(10)) // Timeout for connecting
        .build();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com"))
        .timeout(java.time.Duration.ofSeconds(5)) // Timeout for the request
        .GET()
        .build();
  • connectTimeout(): Sets the maximum time to establish a connection to the server.
  • request.timeout(): Sets the maximum time to wait for the request/response.

4. Handle HTTP Error Responses

Although HTTP error codes like 4xx (client error) or 5xx (server error) do not throw exceptions directly, you can handle them based on the response status code.

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
int statusCode = response.statusCode();
if (statusCode >= 200 && statusCode < 300) {
    System.out.println("Success: " + response.body());
} else if (statusCode >= 400 && statusCode < 500) {
    System.out.println("Client error (4xx): " + response.body());
} else if (statusCode >= 500) {
    System.out.println("Server error (5xx): " + response.body());
} else {
    System.out.println("Unexpected status code: " + statusCode);
}

5. Using Asynchronous API for Better Error Control

The HttpClient supports asynchronous requests using CompletableFuture, which provides an alternative way of handling success and failure scenarios.

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com"))
        .GET()
        .build();

httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(body -> System.out.println("Response received: " + body))
        .exceptionally(e -> {
            System.err.println("Error occurred: " + e.getMessage());
            return null;
        });
  • thenApply: Process the response body.
  • exceptionally: Handle exceptions such as network failures or timeouts during the asynchronous operation.

6. Best Practices

  • Retry Logic: Implement retry logic for transient errors like timeouts or temporary server unavailability. Libraries like Resilience4j can simplify this.
  • Circuit Breakers: Protect your application from overload by using circuit breakers for repeated failures.
  • Logging and Monitoring: Log error details in a structured format for easy monitoring and debugging.
  • Test Handling: Mock failures using libraries like WireMock to test your error-handling logic.

By combining these strategies, you can handle errors and exceptions comprehensively in Java 11 HttpClient.

How do I chain asynchronous calls using Java 11 HttpClient and CompletableFuture?

Chaining asynchronous calls using Java 11’s HttpClient and CompletableFuture can be achieved by leveraging the reactive capabilities of CompletableFuture. The sendAsync method of HttpClient supports asynchronous processing, and you can chain multiple calls together using methods like thenApply, thenCompose, or thenAccept. Here’s a step-by-step example:


Key Concepts Used:

  1. CompletableFuture:
    • Allows for async processing and chaining of dependent tasks.
  2. HttpClient and HttpRequest:
    • The async calls are made using the HttpClient.sendAsync method.
  3. Chaining methods:
    • Use thenApply to transform the response or thenCompose to chain dependent async calls.

Example: Chaining Multiple HTTP Requests

Say we need to:

  1. Fetch data using one API.
  2. Use the response data to make another API call.
  3. Process the final response.

Here’s how you can do that:

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class AsyncChainingExample {

   public static void main(String[] args) {
      HttpClient client = HttpClient.newHttpClient();

      // First API Request
      HttpRequest firstRequest = HttpRequest.newBuilder()
              .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
              .GET()
              .build();

      // First Async Call
      CompletableFuture<Void> future = client.sendAsync(firstRequest, HttpResponse.BodyHandlers.ofString())
              .thenApply(HttpResponse::body) // Extract body from response
              .thenCompose(body -> {
                 System.out.println("First API Response: " + body);

                 // Use data from the first response to make the second API request
                 String secondApiUri = "https://jsonplaceholder.typicode.com/comments?postId=1";
                 HttpRequest secondRequest = HttpRequest.newBuilder()
                         .uri(URI.create(secondApiUri))
                         .GET()
                         .build();

                 return client.sendAsync(secondRequest, HttpResponse.BodyHandlers.ofString());
              })
              .thenApply(HttpResponse::body) // Extract body from second response
              .thenAccept(secondResponse -> {
                 // Final result processing
                 System.out.println("Second API Response: " + secondResponse);
              });

      // Wait for all the tasks to complete
      future.join();
   }
}

Explanation of the Code:

  1. Create the HttpClient:
    • HttpClient.newHttpClient() initializes the HTTP client that will send requests asynchronously.
  2. First API Call:
    • The first API request (firstRequest) is created using HttpRequest.newBuilder.
    • Send the request asynchronously with:
    client.sendAsync(firstRequest, HttpResponse.BodyHandlers.ofString());
    
    • thenApply is used to extract the body of the response.
  3. Second API Call (Chained):
    • In thenCompose, the code prepares and sends the second API request. This ensures that the second API call happens only after the first call completes.
    • The response of this call is again processed by extracting the body.
  4. Response Processing:
    • thenAccept is used at the end of the chain to process the final response.
  5. Waiting for Completion:
    • Since the operations are asynchronous, future.join() blocks the main thread until all the chained calls complete.

Output:

Sample output from the above example (when run):

First API Response: {
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati",
  "body": "quia et suscipit..."
}

Second API Response: [
  {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    ...
  },
  ...
]

Key Functions Used in the Chain:

  1. thenApply(Function)
    • Transforms the result of the previous step (e.g., extract the body).
  2. thenCompose(Function)
    • Used for dependent async calls. Ensures one CompletableFuture waits for another.
  3. thenAccept(Consumer)
    • Consumes the result without returning anything.

Advantages of this Approach:

  • No need for manual thread management.
  • Non-blocking I/O.
  • Easily scalable chaining of async calls.

This is a modern, clean solution for handling asynchronous HTTP requests in Java using HttpClient and CompletableFuture.