HTTP Client API

Quick Summary:

  • HTTP Client.
  • A modern HTTP client API supporting HTTP/2 and WebSocket, which is versatile and more efficient than the older HttpURLConnection.
  • Introduced in Java 9 (incubator) then standardized in Java 11 (2018).

Key Features

  • HTTP/2 Support: Supports the latest HTTP/2 protocol, enabling more efficient network calls with features like multiplexing, header compression, and push promises.
  • WebSocket Support: Facilitates real-time web communications using the WebSocket protocol.
  • Asynchronous and Synchronous Modes: Offers both synchronous and asynchronous programming models, making it suitable for various application needs.
  • Modern Convenience Features: Provides a fluent API for creating requests and responses, improving the readability and maintainability of the code.

Example

Here’s how you can perform a synchronous GET request to fetch data from a server:

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

public class HttpGetExample {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                                          .uri(URI.create("https://api.example.com/data"))
                                          .build();
        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Status Code: " + response.statusCode());
            System.out.println("Received data: " + response.body());
        } catch (Exception e) {
            System.out.println("Error during HTTP call: " + e.getMessage());
        }
    }
}

And here’s what it looked like before this new HTTP Client, using the older HttpURLConnection:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpGetOldExample {
    public static void main(String[] args) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL("https://api.example.com/data");
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");

            int responseCode = connection.getResponseCode();
            System.out.println("Response Code: " + responseCode);

            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder response = new StringBuilder();

            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
            in.close();

            System.out.println("Received data: " + response.toString());
        } catch (IOException e) {
            System.out.println("Error during HTTP call: " + e.getMessage());
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

Differences and Observations:

  • Verbosity: The HttpURLConnection example involves more boilerplate code. You need to manually manage the connection, specify the request method, handle the input stream, and ensure the connection is closed after receiving the response.
  • Exception Handling: Error handling with HttpURLConnection is more cumbersome. You need to explicitly check the HTTP response code to determine if the request was successful or if there were errors, such as a 404 or 500 server error.
  • Stream Handling: Reading the response from HttpURLConnection requires manual setup of a BufferedReader, reading the lines in a loop, and appending each line to a StringBuilder to construct the full response.
  • HTTP/2 Support: One of the major drawbacks of HttpURLConnection is that it does not support HTTP/2 features, such as multiplexing and server push, which are supported by the new HTTP Client API.
  • Recommendations: Although not deprecated, Oracle and the broader Java community recommend using the newer HTTP Client API for new projects or when upgrading existing projects for better performance and capabilities, especially to take advantage of HTTP/2.

👩‍💻 Hands-on Demo: HTTP Client

🔎 Click here to expand

1. Multiple Choice Quiz

Which of the following is NOT a feature of the HTTP Client API introduced in Java 11?

  • A) Asynchronous programming support
  • B) WebSocket programming support
  • C) Automatic handling of cookies
  • D) Synchronous programming support

2. Fill-in-the-Blanks

  • Fill in the blanks to correctly make use of HTTP Client.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpClientExample {
    public static void main(String[] args) {
        HttpClient client = HttpClient._______();
        HttpRequest request = HttpRequest.newBuilder()
                                         .uri(URI.create("https://api.example.com"))
                                         ._____();
        try {
            HttpResponse<String> response = client._____(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response status code: " + response.statusCode());
            System.out.println("Response body: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. Debugging Challenge: Code Correction

Correct the following code snippet.

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newRequest()
                                 .uri(URI.create("https://api.example.com/data"))
                                 .GET()
                                 .build();

4. Catch ‘Em All

We’re gonna use HTTP Client to send a GET request to the Pokémon API to retrieve information about a specific Pokémon, starting with Pikachu, then user’s choice.

  1. In a PokemonFetcherApp-class, use the HTTP Client API to send a GET-request to fetch data about Pikachu from the Pokémon API.
  2. Handle the response by printing the status code and body.
  3. Modify the program to allow the user to input the name of the Pokémon to fetch (the URL is dynamically resolved).
  4. Modify the response-handling logic so that if the response status is OK, only then is the body printed, otherwise if the response status is NOT FOUND then “No Pokémon found with that name: {input name}” is printed, and for all other statuses “Something went wrong” is printed.
  1. (Extra) Modify the response-handling logic so that instead of the whole body, the Pokémon’s name, height and weight are printed.

5. Send a POST Request

Here you will send a POST-request to a test API that accepts user details, and then handle and display the response.

  1. In a PostUserDetailsApp-class, use the HTTP Client API to send a POST-request with a JSON payload to the ReqRes API.
    • Endpoint to use: https://reqres.in/api/users
    • Set the Content-Type header to application/json
    • Send a JSON payload such as:
      {"name": "James Barnes", "job": "Axolotl Masseuse"}
  2. Handle the response by printing the status code and body.
  3. Modify the program to allow the user to input the name and job of the user being sent.

Solutions

🕵️‍♂️ Click here to reveal the solutions

1. Multiple Choice Quiz

Answer: C) Automatic handling of cookies

Explanation: While the HTTP Client API supports both synchronous and asynchronous programming and even WebSocket communications, it does not automatically handle cookies. Developers need to manage cookies manually or use additional libraries.

2. Fill-in-the-Blanks

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

public class HttpClientExample {
    public static void main(String[] args) {
        HttpClient client = HttpClient._______();
        HttpRequest request = HttpRequest.newBuilder()
                                         .uri(URI.create("https://api.example.com"))
                                         ._____();
        try {
            HttpResponse<String> response = client._____(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response status code: " + response.statusCode());
            System.out.println("Response body: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpClientExample {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                                         .uri(URI.create("https://api.example.com"))
                                         .build();
        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response status code: " + response.statusCode());
            System.out.println("Response body: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. Debugging Challenge: Code Correction

Original:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newRequest()
                                 .uri(URI.create("https://api.example.com/data"))
                                 .GET()
                                 .build();
Corrected:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() // Changed from newRequest() to newBuilder()
                                  .uri(URI.create("https://api.example.com/data"))
                                  .GET()
                                  .build();

4. Catch ‘Em All

public class PokemonFetcherApp {
    private static final Scanner KEYBOARD = new Scanner(System.in);

    public static void main(String[] args) {
        String inputName = askForPokemonName();
        HttpResponse<String> response = fetchPokemonByName(inputName);

        System.out.println("Status Code: " + response.statusCode());
        switch (response.statusCode()) {
            case 200:
                System.out.println("Response Body: " + response.body());
                break;
            case 404:
                System.out.println("No Pokémon found with that name: " + inputName);
                break;
            default:
                System.out.println("Something went wrong");
        }
    }

    private static HttpResponse<String> fetchPokemonByName(String name) {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://pokeapi.co/api/v2/pokemon/" + name))
                .build();
        try{
            return client.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    private static String askForPokemonName() {
        System.out.print("Enter a Pokémon name: ");
        String name = KEYBOARD.nextLine().trim();
        while (name.isEmpty()) {
            System.out.print("Please enter a Pokémon name: ");
            name = KEYBOARD.nextLine().trim();
        }
        return name.toLowerCase();
    }
}

5. Send a POST Request

public class PostUserDetailsApp {
    private static final Scanner KEYBOARD = new Scanner(System.in);

    public static void main(String[] args) {
        // Prepare the JSON payload as a String
        String userPayload = askForUserPayload();

        HttpResponse<String> response = postUserDetails(userPayload);

        // Print status code and response body
        System.out.println("Status Code: " + response.statusCode());
        System.out.println("Response Body: " + response.body());
    }


    private static String askForUserPayload() {
        System.out.print("Enter name: ");
        String inputName = KEYBOARD.nextLine();
        System.out.print("Enter job: ");
        String inputJob = KEYBOARD.nextLine();
        return convertUserToJson(inputName, inputJob);
    }

    private static String convertUserToJson(String name, String job) {
        // Create a JSON object with the user details
        return String.format("{\"name\": \"%s\", \"job\": \"%s\"}", name, job);
    }

    private static HttpResponse<String> postUserDetails(String userPayload) {
        // Create the HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://reqres.in/api/users"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(userPayload))
                .build();

        // Send the request and get the response
        try {
            return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Asynchronous Requests

Asynchronous requests are a powerful feature of Java’s HTTP/2 Client API that allow your application to perform non-blocking operations over HTTP. This means your app can continue executing other tasks while waiting for the HTTP response.

What are Asynchronous Requests?

  • Asynchronous requests let your application send HTTP requests and handle responses without waiting for the server to respond before moving on to other tasks. This is handled through futures and callbacks, avoiding blocking the current thread.

Why Use Asynchronous Requests?

  • Efficiency: Improves the performance of your application by freeing up system resources that would otherwise be idle during the HTTP request/response cycle.
  • Better Resource Use: Allows handling of more network requests simultaneously by not blocking threads on network calls.
  • Responsive Applications: Keeps your application responsive, especially important for GUI applications where a long-running task might freeze the user interface.

How to Use Asynchronous Requests?

  • Utilize the HttpClient.sendAsync() method, which sends the HTTP request and immediately returns a CompletableFuture<HttpResponse<T>>. This future will complete once the response is available.
  • Chain callback methods like thenApply, thenAccept, or thenCombine to handle the response once it’s received, without blocking the current thread.

Example

Here’s how to perform an asynchronous GET request with Java’s HTTP/2 Client API, incorporating error handling and response processing:

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

public class HttpGetAsyncExample {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newBuilder().build();
        HttpRequest request = HttpRequest.newBuilder()
                                          .uri(URI.create("https://api.example.com/data"))
                                          .build();

        client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
              .thenApply(response -> {
                  if (response.statusCode() == 200) {
                      return response.body();
                  } else {
                      throw new RuntimeException("Failed: HTTP error code : " + response.statusCode());
                  }
              })
              .exceptionally(e -> "Error: " + e.getMessage())
              .thenAccept(System.out::println)
              .join(); // Wait for the response to ensure JVM doesn't exit prematurely
    }
}

👩‍💻 Hands-on Demo: Asynchronous Requests

🔎 Click here to expand

1. Asynchronously Send a POST Request

  1. Copy+paste your PostUserDetailsApp-class, naming it PostUserDetailsAsyncApp.
  2. Demarcate the start and finish of your main-method with print-statements (see example screenshots).
  1. Copy+paste the “post user payload to API and handle response” logic, and adapt it to run asynchronously.
  2. Notice how, unless you force it to wait, the rest of the main-method keeps being executed (even until completion), while the POST-request is sent in the background asynchronously.
  1. Artificially enforce blocking behaviour by making your asynchronous operation wait until it’s complete.

2. Catch ‘Em All Asynchronously

  1. Copy+paste your PokemonFetcherApp-class, naming it PokemonFetcherAsyncApp.
  2. Adapt your data-fetching logic such that it accepts an array of names and programmatically fetches them all (synchronously and asynchronously).
  3. Make an array of 3 Pokémon and pass them to your logic.
  4. Write some basic time-measurement logic to compare how long it takes for
  1. Notice how haphazardly the responses are handled (order seems random) and how much faster processing multiple asynchronous calls is than synchronous.

Solutions

🕵️‍♂️ Click here to reveal the solutions

1. Asynchronously Send a POST Request

public class PostUserDetailsAsyncApp {
    private static final Scanner KEYBOARD = new Scanner(System.in);

    public static void main(String[] args) {
        // Prepare the JSON payload as a String
        runAsynchronously();
        System.out.println("Finished code in main-method");
    }

    private static void runAsynchronously() {
        System.out.println("Running asynchronously");
        String userPayload = askForUserPayload();
        postUserDetailsAsync(userPayload).thenAccept(response -> handleResponse(response)).join();
    }

    private static void runSynchronously() {
        System.out.println("Running synchronously");
        String userPayload = askForUserPayload();
        HttpResponse<String> response = postUserDetailsSync(userPayload);
        handleResponse(response);
    }

    private static void handleResponse(HttpResponse<String> response) {
        // Print status code and response body
        System.out.println("Status Code: " + response.statusCode());
        System.out.println("Response Body: " + response.body());
    }


    private static String askForUserPayload() {
        System.out.print("Enter name: ");
        String inputName = KEYBOARD.nextLine();
        System.out.print("Enter job: ");
        String inputJob = KEYBOARD.nextLine();
        return convertUserToJson(inputName, inputJob);
    }

    private static String convertUserToJson(String name, String job) {
        // Create a JSON object with the user details
        return String.format("{\"name\": \"%s\", \"job\": \"%s\"}", name, job);
    }

    private static HttpResponse<String> postUserDetailsSync(String userPayload) {
        // Create the HttpRequest
        HttpRequest request = preparePostUserReq(userPayload);

        // Send the request and get the response
        try {
            return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private static CompletableFuture<HttpResponse<String>> postUserDetailsAsync(String userPayload) {
        // Create the HttpRequest
        HttpRequest request = preparePostUserReq(userPayload);

        // Send the request asynchronously and get the response
        return HttpClient.newHttpClient().sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(res -> {
                    if (res.statusCode() / 100 == 2) {
                        return res;
                    } else {
                        throw new RuntimeException("Request failed with status code " + res.statusCode());
                    }
                });

    }

    private static HttpRequest preparePostUserReq(String userPayload) {
        return HttpRequest.newBuilder()
                .uri(URI.create("https://reqres.in/api/users"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(userPayload))
                .build();
    }
}

2. Catch ‘Em All Asynchronously

public class PokemonFetcherAsyncApp {
    private static final Scanner KEYBOARD = new Scanner(System.in);

    public static void main(String[] args) {
        String[] names = {"bulbasaur", "charmander", "squirtle"};
        System.out.println("▶ Synchronous test:");
        runFetchPokemonsSyncTest(names);
        System.out.println("▶ Asynchronous test:");
        runFetchPokemonsAsyncTest(names);
    }

    private static void runFetchPokemonsSyncTest(String[] names) {
        runTimedFunction(() -> {
            for (String name : names) {
                HttpResponse<String> response = fetchPokemonByNameSync(name);
                handleResponseForPokemonWithName(response, name);
            }
        });
    }

    private static void runFetchPokemonsAsyncTest(String[] names) {
        runTimedFunction(() -> {
            // Create a list of CompletableFuture objects from the names array
            List<CompletableFuture<Void>> futures = Arrays.stream(names)
                    .map(n -> fetchPokemonByNameAsync(n).thenAccept(response -> handleResponseForPokemonWithName(response, n)))
                    .collect(Collectors.toList());

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

    private static void runTimedFunction(Runnable runnable) {
        LocalDateTime startTime = LocalDateTime.now();
        runnable.run();
        LocalDateTime endTime = LocalDateTime.now();
        Duration duration = Duration.between(startTime, endTime);

        long minutes = duration.toMinutes();
        long seconds = duration.minusMinutes(minutes).getSeconds();
        long milliseconds = duration.minusSeconds(seconds).toMillis();

        System.out.printf("\t⌚Total duration: %d minutes, %d seconds, and %d milliseconds%n", minutes, seconds, milliseconds);
    }

    private static void handleResponseForPokemonWithName(HttpResponse<String> response, String name) {
        System.out.print("Status Code: " + response.statusCode() + " | ");
        switch (response.statusCode()) {
            case 200:
                System.out.printf("'%s' successfully fetched.\n", name);
                break;
            case 404:
                System.out.println("No Pokémon found with that name: " + name);
                break;
            default:
                System.out.println("Something went wrong");
        }
    }

    private static CompletableFuture<HttpResponse<String>> fetchPokemonByNameAsync(String name) {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = prepareGetPokemonByNameReq(name);

        // Returning a CompletableFuture<HttpResponse<String>>
        System.out.printf("Fetching '%s' asynchronously...%n", name);
        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(response -> {
                    // You can process the response further here if needed
                    return response;
                })
                .exceptionally(e -> {
                    // Exception handling logic here
                    System.err.println("Failed to fetch data: " + e.getMessage());
                    throw new RuntimeException("Failed to fetch Pokemon data", e);
                });
    }

    private static HttpResponse<String> fetchPokemonByNameSync(String name) {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = prepareGetPokemonByNameReq(name);
        try {
            System.out.printf("Fetching '%s' synchronously...%n", name);
            return client.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static HttpRequest prepareGetPokemonByNameReq(String name) {
        return HttpRequest.newBuilder()
                .uri(URI.create("https://pokeapi.co/api/v2/pokemon/" + name))
                .method("GET", HttpRequest.BodyPublishers.noBody())
                .build();
    }

    private static String askForPokemonName() {
        System.out.print("Enter a Pokémon name: ");
        String name = KEYBOARD.nextLine().trim();
        while (name.isEmpty()) {
            System.out.print("Please enter a Pokémon name: ");
            name = KEYBOARD.nextLine().trim();
        }
        return name.toLowerCase();
    }
}

Summary

  • Introduction of HTTP/2 Client API: Replaces the older HttpURLConnection with a modern API that supports HTTP/2 and WebSocket, enhancing efficiency and functionality. Introduced as an incubator feature in Java 9 and standardized in Java 11 under JEPs 110 and 321.
  • Simplification and Efficiency: The new HTTP Client API reduces the verbosity of the code compared to HttpURLConnection. It automates connection management, stream handling, and error processing, making the code more readable and maintainable.
  • Support for HTTP/2: Enables advanced HTTP/2 features like multiplexing, header compression, and server push, which optimize the performance and responsiveness of network calls.
  • Asynchronous and Synchronous Modes: Offers flexibility in request handling with both asynchronous and synchronous methods, catering to different use cases and enhancing application performance.