6 minutes
Let’s make some calls
Introduction
One of the most important things in programming is making HTTP calls to talk with some API. Java has very good support for this, and in this post, we will see how to call some HTTP endpoints using Java’s HttpClient
class. This class is part of the Java 11 standard library, so you don’t need any external dependencies to use it.
A closer look at the HTTP client
Besides the HttpClient
class, there are a few other classes that we will use in this post:
HttpRequest
: This class represents an HTTP requestHttpResponse
: This class represents an HTTP responseHttpRequest.BodyPublishers
: This class contains static methods to create request bodiesHttpResponse.BodyHandlers
: This class contains static methods to create response bodies
The new HTTP APIs can be found in the java.net.http
package.
It’s important to note that the new HTTP client is intended to replace the legacy HttpURLConnection
class, which is still part of Java but considered outdated. The modern HttpClient
not only simplifies making HTTP requests, but also adds powerful features like asynchronous calls, native WebSocket support, and HTTP/2 compatibility—making it a more robust and developer-friendly option overall.
The endpoint we will be using for the following examples is the JSONPlaceholder API, which is a free online REST API that you can use for testing and prototyping.
The steps are similar for all the requests we will be making:
- Create an
HttpClient
instance; can be reused for multiple requests - Create an
HttpRequest
instance - Send the request and get the response
- Process the response, i.e. either return the raw body or parse it into a Java object
Creating an HTTP client
import java.net.http.HttpClient;
try (var client = HttpClient.newHttpClient()) {
// Use the client to make requests
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
Since HttpClient
implements java.lang.AutoCloseable
we need to close the resource when we are done with it, the easiest way to do this is by wrapping the instantiation with a try-with-resources
block.
Creating an HTTP request
GET request
public static final String GET_SINGLE_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";
var request = HttpRequest.newBuilder()
.uri(new URI(GET_SINGLE_ENDPOINT))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
int responseStatusCode = response.statusCode();
To fetch all the posts, instead of a single post, we can do the following:
public static final String GET_ALL_ENDPOINT = "https://jsonplaceholder.typicode.com/posts";
var request = HttpRequest.newBuilder().uri(new URI(GET_ALL_ENDPOINT)).GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
If the raw response body is a JSON string, we can use a library like Jackson or Gson to parse it into a Java object.
import com.fasterxml.jackson.databind.ObjectMapper;
var objectMapper = new ObjectMapper();
Post[] posts = objectMapper.readValue(responseBody, Post[].class);
var gson = new Gson();
Post[] posts = gson.fromJson(responseBody, Post[].class);
Make sure to add the Jackson or Gson dependency to your pom.xml
file:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
For the following code examples, making the request and getting the raw body is the same.
POST request
private static final String POST_ENDPOINT = "https://jsonplaceholder.typicode.com/posts";
var body = """
{
"title": "foo",
"body": "bar",
"userId": 1
}
""";
var request = HttpRequest.newBuilder()
.uri(new URI(POST_ENDPOINT))
.POST(HttpRequest.BodyPublishers.ofString(body))
.headers("Content-Type", "application/json")
.build();
For a POST with no body, simply use HttpRequest.BodyPublishers.noBody()
.
PUT request
private static final String PUT_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";
var body = """
{
"id":1,
"title": "FOO",
"body": "BAR",
"userId": 1
}
""";
var request = HttpRequest.newBuilder()
.uri(URI.create(PUT_ENDPOINT))
.PUT(HttpRequest.BodyPublishers.ofString(body))
.headers("Content-Type", "application/json")
.build();
PATCH request
private static final String PATCH_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";
var body = """
{
"title" : "dummy title"
}
""";
var request = HttpRequest.newBuilder()
.uri(new URI(PATCH_ENDPOINT))
.method("PATCH", HttpRequest.BodyPublishers.ofString(body))
.headers("Content-Type", "application/json")
.build();
DELETE request
private static final String DELETE_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";
var request = HttpRequest.newBuilder()
.uri(new URI(DELETE_ENDPOINT))
.DELETE()
.build();
Making asynchronous calls
Making asynchronous calls is very similar to making synchronous calls. The only difference is that we need to use the sendAsync
method instead of the send
method, and deal with the CompletableFuture
that is returned.
private static void makeAsyncRequest(HttpClient client, HttpRequest request) {
// send the request asynchronously
var future = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
// process the response
.thenApply(App::handleResponse)
// consume the response, by printing the first 5 posts
.thenAccept(posts -> Arrays.stream(posts).limit(5).forEach(System.out::println));
// wait for the future to complete, before proceeding with the program
future.join();
}
private static Post[] handleResponse(HttpResponse<String> response) {
int statusCode = response.statusCode();
System.out.println("response.statusCode() = " + statusCode);
if (statusCode == 200) {
try {
return new ObjectMapper().readValue(response.body(), Post[].class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
} else {
System.out.println("Unsuccessful response.");
}
return new Post[]{};
}
Specifying other options
When building the client, we can specify various options, such as:
- the proxy to use:
.proxy(ProxySelector.getDefault())
- the redirect policy:
.followRedirects(HttpClient.Redirect.ALWAYS)
- the authenticator:
.authenticator(auth)
- the executor:
.executor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()))
When building the request, we can specify various options, such as:
- the timeout:
.timeout(Duration.ofSeconds(1))
- the headers:
.header("Content-Type", "application/json")
- the protocol version:
.version(HttpClient.Version.HTTP_2)
- the body publishers:
.POST(HttpRequest.BodyPublishers.ofString(body))
There are a couple of other body publishers that we can use, such as:
HttpRequest.BodyPublishers.ofFile()
: body content taken from the fileHttpRequest.BodyPublishers.ofInputStream()
: body content taken from an input streamHttpRequest.BodyPublishers.ofByteArray()
: body content taken from the byte array
When sending the request we can specify how the response body should be handled, such as:
HttpResponse.BodyHandlers.ofString()
: returns the response body as a stringHttpResponse.BodyHandlers.ofByteArray()
: returns the response body as a byte arrayHttpResponse.BodyHandlers.ofFile()
: returns the response body as a fileHttpResponse.BodyHandlers.ofInputStream()
: returns the response body as an input streamHttpResponse.BodyHandlers.ofPublisher()
: returns the response body as a publisherHttpResponse.BodyHandlers.ofLines()
: returns the response body as a stream of lines
Authentication
When creating the client, we can specify an Authenticator
to use for authentication. The Authenticator
is a class that provides a way to authenticate requests. We can extend it, and override the getPasswordAuthentication
method to provide the credentials.
import java.net.Authenticator;
import java.net.PasswordAuthentication;
public class MyAuthenticator extends Authenticator {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
"username",
"password".toCharArray()
);
}
}
Alternatively, we can set the Authorization
header in the request, and use a basic authentication scheme or a bearer token.
// for the basic authentication scheme
var auth = Base64.getEncoder().encodeToString("username:password".getBytes());
var getRequest = HttpRequest.newBuilder(new URI("<endpoint>"))
// set a bearer token
.header("Authorization", "Bearer <token>")
// or set a basic authentication scheme
.header("Authorization", "Basic " + auth)
.GET()
.build();
Conclusion
In this post, we have seen how to make HTTP calls using Java’s HttpClient
class. We have seen how to create a client, create a request, and send the request. We have also seen how to make asynchronous calls and specify various options for the client and the request.