Since Java 11, you can use HttpClient API to execute non-blocking HTTP requests and handle responses through CompletableFuture, which can be chained to trigger dependant actions

The following example sends an HTTP GET request and retrieves its response asynchronously with HttpClient and CompletableFuture

@Test
public void getAsync() {  
    HttpClient client = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/resource"))
        .header("Accept", "application/json")
        .build();

    int statusCode = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApplyAsync(HttpResponse::statusCode)
        .join();

    assertThat(statusCode).isEqualTo(200);
}

Let's walk through this tutorial to explore in more details

What you'll need

  • JDK 11+
  • Maven 3+
  • Your favorite IDE

Tech stack

  • Java 11+ for learning HttpClient API
  • JUnit 4 for writing test cases
  • WireMock for mocking Http server
  • AssertJ for verifying test result

Create a new HttpClient

You can use HttpClient.newBuilder() to create a new HttpClient instance and configure options through fluent APIs

The below example gives you full HttpClient configuration options

@Test
public void createAnHTTPClient() throws NoSuchAlgorithmException {  
    HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .proxy(ProxySelector.getDefault())
        .followRedirects(HttpClient.Redirect.NEVER)
        .authenticator(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication("user", "pass".toCharArray());
            }
        })
        .cookieHandler(new CookieManager())
        .executor(Executors.newFixedThreadPool(2))
        .priority(1)
        .sslContext(SSLContext.getDefault())
        .sslParameters(new SSLParameters())
        .connectTimeout(Duration.ofSeconds(1))
        .build();

    assertThat(client.connectTimeout()).get().isEqualTo(Duration.ofSeconds(1));
}
  • version(HttpClient.Version.HTTP_2)) preferred HTTP version 2, fall back to version 1.1 if the server is not supported

  • followRedirects(HttpClient.Redirect.NEVER) never redirect an HTTP request. This is the default option, other options are Redirect.ALWAYS (always redirect), and Redirect.NORMAL (always redirect except from HTTPS URL to HTTP URLS)

  • authenticator(Authenticator) set a non-preemptive Basic Authentication

  • cookieHandler(new CookieManager()) enables a cookie handler, disabled if this option is ignored

  • connectTimeout(Duration.ofSeconds(1)) sets the connect timeout duration for an HttpClient instance. HttpConnectTimeoutException will be thrown if the connection cannot be established within the given duration

You can also use HttpClient.newHttpClient() to create a new HttpClient with default settings. It is a short form of HttpClient.newBuilder().build()

@Test
public void createADefaultHTTPClient() {  
    HttpClient client = HttpClient.newHttpClient();

    assertThat(client.version()).isEqualTo(HttpClient.Version.HTTP_2);
    assertThat(client.followRedirects()).isEqualTo(HttpClient.Redirect.NEVER);
    assertThat(client.proxy()).isEmpty();
    assertThat(client.connectTimeout()).isEmpty();
    assertThat(client.cookieHandler()).isEmpty();
    assertThat(client.authenticator()).isEmpty();
    assertThat(client.executor()).isEmpty();
}

The created HttpClient is immutable, so thread-safe, and can be used to send multiple requests

Build a new HttpRequest

You can build a new HttpRequest with HttpRequest.newBuilder()

@Test
public void buildAnHTTPRequest() {  
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/resource"))
        .POST(HttpRequest.BodyPublishers.noBody())
        .version(HttpClient.Version.HTTP_2)
        .header("Accept", "application/json")
        .timeout(Duration.ofMillis(500))
        .build();

    assertThat(request.timeout().get()).isEqualTo(Duration.ofMillis(500));
}
  • uri(java.net.URI) sets the URI of the sending request

  • The HTTP request method is set by using GET(), POST(BodyPublisher), PUT(BodyPublisher), DELETE(BodyPublisher) or method(String, BodyPublisher)

    GET() is the default request method

  • version(HttpClient.Version) sets the prefered HTTP version for the executing request. The actual using version should be checked in the corresponding HttpResponse

    If this option is ignored, the version in the sending HttpClient will be applied

  • header(String name, String value) adds the given name-value pair to the set of headers for the executing request

    IllegalArgumentException will be thrown if the header name or value is not valid

  • timeout(Duration) sets a read timeout for this request

    HttpTimeoutException will be thrown if the response is not received within the specified timeout

    If this option is ignored, the read timeout will be infinite

Send an HttpRequest Synchronously

Use send() method of an HttpClient instance to execute an HttpRequest synchronously

@Test
public void getSync() throws IOException, InterruptedException {  
    HttpClient client = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/resource"))
        .header("Accept", "application/json")
        .build();

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

    assertThat(response.statusCode()).isEqualTo(200);
}

The above example sends a GET HTTP request synchronously with send()

send() blocks the current thread if necessary to get the response. The returned HttpResponse<T> contains the response status, headers, and body

Send an HttpRequest Asynchronously

Use sendAsync() method of an HttpClient instance to execute an HttpRequest asynchronously

@Test
public void postAsync() {  
    HttpClient client = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/resource"))
        .header("Accept", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("ping!"))
        .build();

    CompletableFuture<HttpResponse<String>> completableFuture =
        client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
    completableFuture
        .thenApplyAsync(HttpResponse::headers)
        .thenAcceptAsync(System.out::println);
    HttpResponse<String> response = completableFuture.join();

    assertThat(response.statusCode()).isEqualTo(200);
}

The above example sends a POST request asynchronously with sendAsync

sendAsync doesn't block the current thread like send, it returns a CompletableFuture immediately. If completed successfully, it completes with an HttpResponse that contains status, headers, and body

Query Parameters

HttpClient doesn't come with a URI components builder. However, you can add query string parameters to the URL while creating a new URI

URI.create("http://localhost:8081/test/resource?a=b")  

Post JSON

There's no built-in JSON support. You can use Jackson or Gson to parse Object to String and vice versa

The following example sends an HTTP POST request through HttpClient with JSON data which is serialized and deserialized by Jackson ObjectMapper

@Test
public void postJson() throws IOException, InterruptedException {  
    HttpClient client = HttpClient.newHttpClient();

    Book book = new Book(1, "Java HttpClient in practice");
    String body = objectMapper.writeValueAsString(book);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/resource"))
        .header("Accept", "application/json")
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

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

    assertThat(response.statusCode()).isEqualTo(200);
    assertThat(objectMapper.readValue(response.body(), Book.class).id).isEqualTo(1);
}

Basic Authentication

Add basic authentication to HttpClient with the following approaches

  • Use authenticator() method of an HttpClient instance
HttpClient client = HttpClient.newBuilder()  
    .authenticator(new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication("user", "pass".toCharArray());
        }
    })
    .build();
  • Add Authorization header to an HttpRequest instance. Use this option when a preemptive basic authentication is required by a server like WireMock
@Test
public void basicAuthentication() throws IOException, InterruptedException {  
    HttpClient client = HttpClient.newHttpClient();

    String encodedAuth = Base64.getEncoder()
        .encodeToString(("user" + ":" + "pass").getBytes(StandardCharsets.UTF_8));

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/secure"))
        .header("Authorization", "Basic " + encodedAuth)
        .build();

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

    assertThat(response.statusCode()).isEqualTo(200);
}

Cookie Handler

Cookies are disabled by default. To enable, create a new CookieManager and add it to cookieHandler method of an HttpClient

HttpClient client = HttpClient.newBuilder()  
        .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
        .build();

There're 3 cookie policies, namely ACCEPT_ALL, ACCEPT_NONE, and ACCEPT_ORIGINAL_SERVER (default)

@Test
public void cookie() throws IOException, InterruptedException {  
    HttpClient client = HttpClient.newBuilder()
        .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
        .build();

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/set-cookie"))
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    String cookie = response.headers().firstValue("Set-Cookie").get();
    assertThat(HttpCookie.parse(cookie).get(0).getName()).isEqualTo("SID");

    request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8081/test/resource"))
        .header("Accept", "application/json")
        .build();
    response = client.send(request, HttpResponse.BodyHandlers.ofString());
    assertThat(response.statusCode()).isEqualTo(200);
}

Conclusion

In this tutorial, we learned how to create a new HttpClient instance, configure and use it to build and execute an HTTP request synchronously and asynchronously. You can find the full source code as below


Share to social

Van N.

Van N. is a software engineer, creator of HelloKoding. He loves coding, blogging, and traveling. You may find him on GitHub and LinkedIn