Typesafe HTTP Clients with OkHttp and Retrofit

Typesafe HTTP Clients with OkHttp and Retrofit

Table Of Contents

Developers use HTTP Clients to communicate with other applications over the network. Over the years, multiple HTTP Clients have been developed to suit various application needs.

In this article, we will focus on Retrofit, one of the most popular type-safe Http clients for Java and Android.

Example Code

This article is accompanied by a working code example on GitHub.

What is OkHttp?

OkHttp is an efficient HTTP client developed by Square. Some of its key advantages are:

  • HTTP/2 support
  • Connection pooling (helps reduce request latency)
  • GZIP compression (saves bandwidth and speeds up interaction)
  • Response Caching
  • Silent recovery from connection problems
  • Support for synchronous and asynchronous calls

What is Retrofit?

Retrofit is a high-level REST abstraction built on top of OkHttp. When used to call REST applications, it greatly simplifies API interactions by parsing requests and responses into POJOs.

In the further sections, we will work on creating a Retrofit client and look at how to incorporate the various features that OkHttp provides.

Setting up a REST Server

We will use a sample REST-based Library Application that can fetch, create, update and delete books and authors. You can check out the source code on GitHub and run the application yourself if you want.

This library application is a Spring Boot service that uses Maven for building and HSQLDB as the underlying database. The Maven Wrapper bundled with the application will be used to start the service:

mvnw clean verify spring-boot:run (for Windows)
./mvnw clean verify spring-boot:run (for Linux)

Now, the application should successfully start:

[     main] com.reflectoring.library.Application  : Started application in 6.94 seconds (JVM running for 7.611)

Swagger is a set of tools that describes an API structure by creating user-friendly documentation and helps develop and describe RESTful APIs. This application uses the Swagger documentation that can be viewed at http://localhost:8090/swagger-ui.html

The documentation should look like this:

settings

Swagger also allows us to make calls to the REST endpoints. Before we can do this, we need to add basic authentication credentials as configured in application.yaml:

settings

Now, we can hit the REST endpoints successfully. Sample JSON requests are available in README.md file in the application codebase.

settings settings

Once the POST request to add a book to the library is successful, we should be able to make a GET call to confirm this addition.

settings

Now that our REST service works as expected, we will move on to introduce another application that will act as a REST client making calls to this service. In the process, we will learn about Retrofit and its various features.

Building a REST Client with Retrofit

The REST Client application will be a Library Audit application that exposes REST endpoints and uses Retrofit to call our previously set up Library application. The result is then audited in an in-memory database for tracking purposes.

Adding Retrofit dependencies

With Maven:

<dependency>
	<groupId>com.squareup.retrofit2</groupId>
	<artifactId>retrofit</artifactId>
	<version>2.5.0</version>
</dependency>
<dependency>
	<groupId>com.squareup.retrofit2</groupId>
	<artifactId>converter-jackson</artifactId>
	<version>2.5.0</version>
</dependency>

With Gradle:

dependencies {  
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-jackson:2.5.0'
}

Quick Guide to Setting up a Retrofit Client

Every Retrofit client needs to follow the three steps listed below:

Creating the Model Objects for Retrofit

We will take the help of the Swagger documentation in our REST service to create model objects for our Retrofit client.

settings

We will now create corresponding model objects in our client application:

@Getter
@Setter
@NoArgsConstructor
public class AuthorDto {

    @JsonProperty("id")
    private long id;

    @JsonProperty("name")
    private String name;

    @JsonProperty("dob")
    private String dob;

}
@Getter
@Setter
@NoArgsConstructor
public class BookDto {
    @JsonProperty("bookId")
    private long id;

    @JsonProperty("bookName")
    private String name;

    @JsonProperty("publisher")
    private String publisher;

    @JsonProperty("publicationYear")
    private String publicationYear;

    @JsonProperty("isCopyrighted")
    private boolean copyrightIssued;

    @JsonProperty("authors")
    private Set<AuthorDto> authors;
}

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LibResponse {
    private String responseCode;

    private String responseMsg;
}

We take advantage of Lombok to generate getters, setters, and constructors for us (@Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor). You can read more about Lombok in our article.

Creating the Client Interface

To create the retrofit interface, we will map every service call with a corresponding interface method as shown in the screenshot below.

settings
public interface LibraryClient {

    @GET("/library/managed/books")
    Call<List<BookDto>> getAllBooks(@Query("type") String type);

    @POST("/library/managed/books")
    Call<LibResponse> createNewBook(@Body BookDto book);

    @PUT("/library/managed/books/{id}")
    Call<LibResponse> updateBook(@Path("id") Long id, @Body BookDto book);

    @DELETE("/library/managed/books/{id}")
    Call<LibResponse> deleteBook(@Path("id") Long id);
}

Creating a Retrofit Client

We will use the Retrofit Builder API to create an instance of the Retrofit client for us:

@Configuration
@EnableConfigurationProperties(ClientConfigProperties.class)
public class RestClientConfiguration {

    @Bean
    public LibraryClient libraryClient(ClientConfigProperties props) {
        OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
                .addInterceptor(new BasicAuthInterceptor(props.getUsername(), props.getPassword()))
                .connectTimeout(props.getConnectionTimeout(), TimeUnit.SECONDS)
                .readTimeout(props.getReadWriteTimeout(), TimeUnit.SECONDS);

        return new Retrofit.Builder().client(httpClientBuilder.build())
                .baseUrl(props.getEndpoint())
                .addConverterFactory(JacksonConverterFactory.create(new ObjectMapper()))
                .build().create(LibraryClient.class);

    }

}

Here, we have created a Spring Boot configuration that uses the Retrofit Builder to create a Spring bean that we can then use in other classes.

We will deep-dive into each of the three steps listed above in the next section.

Using Retrofit in Detail

This section will focus on the annotations, Retrofit classes, and features that will help us create a flexible and easy-to-configure REST client.

Building a Client Interface

In this section, we will look at how to build the client interface. Retrofit supports annotations @GET, @POST, @PUT, @DELETE, @PATCH, @OPTIONS, @HEAD which we use to annotate our client methods as shown below:

Path Parameters

Along with the mentioned annotations, we specify the relative path of the REST service endpoint. To make this relative URL more dynamic we use parameter replacement blocks as shown below:

@PUT("/library/managed/books/{id}")
Call<LibResponse> updateBook(@Path("id") Long id, @Body BookDto book);

To pass the actual value of id, we annotate a method parameter with the @Path annotation so that the call execution will replace {id} with its corresponding value.

Query Parameters

We can specify the query parameters in the URL directly or add a @Query-annotated param to the method:

 @GET("/library/managed/books?type=all")
// OR
 @GET("/library/managed/books")
 Call<List<BookDto>> getAllBooks(@Query("type") String type);

Multiple Query Parameters

If the request needs to have multiple query parameters, we can use @QueryMap:

@GET("/library/managed/books")
Call<List<BookDto>> getAllBooks(@QueryMap Map<String, String> options);

Request Body

To specify an object as HTTP request body, we use the @Body annotation:

@POST("/library/managed/books")
Call<LibResponse> createNewBook(@Body BookDto book);

Headers

To the Retrofit interface methods, we can specify static or dynamic header parameters. For static headers, we can use the @Headers annotation:

@Headers("Accept: application/json")
@GET("/library/managed/books")
Call<List<BookDto>> getAllBooks(@Query("type") String type);

We could also define multiple static headers inline:

@Headers({
    "Accept: application/json",
    "Cache-Control: max-age=640000"})
@GET("/library/managed/books")
Call<List<BookDto>> getAllBooks(@Query("type") String type);

To pass dynamic headers, we specify them as method parameters annotated with the @Header annotation:

@GET("/library/managed/books/{requestId}")
Call<BookDto> getAllBooksWithHeaders(@Header("requestId") String requestId);

For multiple dynamic headers, we use @HeaderMap.

All Retrofit responses are wrapped in a Call object. It supports both blocking and non-blocking requests.

Using the Retrofit Builder API

The Builder API on Retrofit allows for customization of our HTTP client. Let’s take a closer look at some configuration options.

Configuring Timeout Settings

We can set timeouts on the underlying HTTP client. However, setting up these values is optional. If we do not specify the timeouts, default settings apply.

  • Connection timeout: 10 sec
  • Read timeout: 10 sec
  • Write timeout: 10 sec

To override these defaults, we need to set up OkHttpClient as shown below:

OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
        .connectTimeout(props.getConnectionTimeout(), TimeUnit.SECONDS)
        .readTimeout(props.getReadWriteTimeout(), TimeUnit.SECONDS);

return new Retrofit.Builder().client(httpClientBuilder.build())
        .baseUrl(props.getEndpoint())
        .addConverterFactory(JacksonConverterFactory.create(new ObjectMapper()))
        .build().create(LibraryClient.class);

Here, the timeout values are as specified in application.yaml.

Using Converters

By default, Retrofit can only deserialize HTTP bodies into OkHttp’s ResponseBody type and its RequestBody type for @Body. With converters, the requests and responses can be wrapped into Java objects.

Commonly used convertors are:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson

To make use of these converters, we need to make sure their corresponding build dependencies are included. Then we can add them to the respective converter factory.

In the following example, we have used Jackson’s ObjectMapper() to map requests and responses to and from JSON:

new Retrofit.Builder().client(httpClientBuilder.build())
        .baseUrl(props.getEndpoint())
        .addConverterFactory(JacksonConverterFactory.create(new ObjectMapper()))
        .build().create(LibraryClient.class);

Adding Interceptors

Interceptors are a part of the OkHttp library that intercepts requests and responses. They help add, remove or modify metadata. OkHttp interceptors are of two types:

  • Application Interceptors - Configured to handle application requests and responses
  • Network Interceptors - Configured to handle network focused scenarios

Let’s take a look at some use-cases where interceptors are used:

Basic Authentication

Basic Authentication is one of the commonly used means to secure endpoints. In our example, the REST service is secured. For the Retrofit client to make authenticated REST calls, we will create an Interceptor class as shown:

public class BasicAuthInterceptor implements Interceptor {

    private final String credentials;

    public BasicAuthInterceptor(String user, String password) {
        this.credentials = Credentials.basic(user, password);
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request authenticatedRequest = request.newBuilder()
                .header("Authorization", credentials).build();
        return chain.proceed(authenticatedRequest);
    }

}

Next, we will add this interceptor to the Retrofit configuration client.

OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
        .addInterceptor(new BasicAuthInterceptor(
                props.getUsername(), 
                props.getPassword()));

The username and password configured in the application.yaml will be securely passed to the REST service in the Authorization header. Adding this interceptor ensures that the Authorization header is attached to every request triggered.

Logging

Logging interceptors print requests, responses, header data and additional information. OkHttp provides a logging library that serves this purpose. To enable this, we need to add com.squareup.okhttp3:logging-interceptor as a dependency. Further, we need to add this interceptor to our Retrofit configuration client:

HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)

With these additions, when we trigger requests, the logs will look like this:

settings

Various levels of logging are available such as BODY, BASIC, HEADERS. We can customize them to the level we need.

In the previous sections, we have seen how to add headers to the client interface. Another way to add headers to requests and responses is via interceptors. We should consider adding interceptors for headers if we need the same common headers to be passed to every request or response:

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {  
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();

        // Request customization: add request headers
        Request.Builder requestBuilder = request.newBuilder()
                .header("Cache-Control", "no-store");

        return chain.proceed(requestBuilder.build());
    }
});

Note that if the request already creates the Cache-Control header, .header() will replace the existing header. There is also a .addHeader() method available that allows us to add multiple values to the same header. For instance:

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {  
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();

        // Request customization: add request headers
        Request.Builder requestBuilder = request.newBuilder()
                .addHeader("Cache-Control", "no-store");
                .addHeader("Cache-Control", "no-cache");

        return chain.proceed(requestBuilder.build());
    }
});

With the above code, the header added will be

Cache-Control: no-store, no-cache
Caching

For applications, caching can help speed up response times. With the combination of caching and network interceptor configuration, we can retrieve cached responses when there is a network connectivity issue. To configure this, we first implement an Interceptor:

public class CacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());

        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(1, TimeUnit.MINUTES) // 1 minutes cache
                .build();

        return response.newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                .header("Cache-Control", cacheControl.toString())
                .build();
    }
}

Here the Cache-Control header is telling the client to cache responses for the configured maxAge. Next, we add this interceptor as a network interceptor and define an OkHttp cache in the client configuration.

Cache cache = new Cache(new File("cache"), 10 * 1024 * 1024);
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
        .addInterceptor(new BasicAuthInterceptor(props.getUsername(), props.getPassword()))
        .cache(cache)
        .addNetworkInterceptor(new CacheInterceptor())
        .addInterceptor(interceptor)
        .connectTimeout(props.getConnectionTimeout(), TimeUnit.SECONDS)
        .readTimeout(props.getReadWriteTimeout(), TimeUnit.SECONDS);

Note: Caching in general applies to GET requests only. With this configuration, the GET requests will be cached for 1 minute. The cached responses will be served during the 1 minute timeframe even if the network connectivity is down.

Custom Interceptors

As explained in the previous sections, BasicAuthInterceptor, CachingInterceptor are all examples of custom interceptors created to serve a specific purpose. Custom interceptors implement the OkHttp Interceptor interface and implement the method intercept().

Next, we configure the interceptor (either as an Application interceptor or Network interceptor). This will make sure the interceptors are chained and called before the end-to-end request is processed.

Note: If multiple interceptors are defined, they are called in sequence. For instance, a Logging interceptor must always be defined as the last interceptor to be called in the chain, so that we do not miss any critical logging during execution.

Using the REST Client to Make Synchronous or Asynchronous Calls

The REST client we configured above can call the service endpoints in two ways:

Synchronous calls

To make a synchronous call, the Call interface provides the execute() method. Since execute() method runs on the main thread, the UI is blocked till the execution completes.

Response<BookDto> allBooksResponse = libraryClient.getAllBooksWithHeaders(bookRequest).execute();
if (allBooksResponse.isSuccessful()) {
    books = allBooksResponse.body();
    log.info("Get All Books : {}", books);
    audit = auditMapper.populateAuditLogForGetBook(books);
} else {
    log.error("Error calling library client: {}", allBooksResponse.errorBody());
    if (Objects.nonNull(allBooksResponse.errorBody())) {
        audit = auditMapper.populateAuditLogForException(
                null, HttpMethod.GET, allBooksResponse.errorBody().string());
    }
}

The methods that help us further process the response are:

  • isSuccessful(): Helps determine if the response HTTP status code is 2xx.
  • body(): On success, returns the response body. In the example above, the response gets mapped to a BookDto object.
  • errorBody(): When the service returns a failure response, this method gives us the corresponding error object. To further extract the error message, we use the errorBody().string().

Asynchronous Calls

To make an asynchronous call, the Call interface provides the enqueue() method. The request is triggered on a separate thread and it does not block the main thread processing.

public void getBooksAsync(String bookRequest) {
    Call<BookDto> bookDtoCall = libraryClient.getAllBooksWithHeaders(bookRequest);
    bookDtoCall.enqueue(new Callback<>() {
        @Override
        public void onResponse(Call<BookDto> call, Response<BookDto> response) {
            if (response.isSuccessful()) {
                log.info("Success response : {}", response.body());
            } else {
                log.info("Error response : {}", response.errorBody());
            }
        }

        @Override
        public void onFailure(Call<BookDto> call, Throwable throwable) {
            log.error("Network error occured : {}", throwable.getLocalizedMessage());
        }
    });
}

We provide implementations to the methods of the Callback interface. The onResponse() handles valid HTTP responses (both success and error) and onFailure() handles network connectivity issues.

We have now covered all the basic components that will help us create a working Retrofit client in a Spring Boot application. In the next section, we will look at mocking the endpoints defined in the Retrofit client.

Mocking an OkHttp REST Client

For writing Unit tests, we will use the Spring Boot Test framework in combination with Mockito and Retrofit Mock. We will include the Retrofit Mock dependency with Maven:

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit-mock</artifactId>
  <version>2.5.0</version>
  <scope>test</scope>
</dependency>

Gradle:

testImplementation group: 'com.squareup.retrofit2', name: 'retrofit-mock', version: '2.5.0'

Next, we will test the service methods. Here we will focus on mocking the Retrofit client calls. First we will use Mockito to mock libraryClient.

@Mock
private LibraryClient libraryClient;

Now, we will mock the client methods and return a static object. Further we will use retrofit-mock to wrap the response into a Call object using Calls.response. Code snippet is as shown below:

String booksResponse = getBooksResponse("/response/getAllBooks.json");
List<BookDto> bookDtoList =
  new ObjectMapper().readValue(booksResponse, new TypeReference<>(){});
when(libraryClient.getAllBooks("all"))
        .thenReturn(Calls.response(bookDtoList));

Calls.response automatically wraps the Call response as successful. To test error scenarios, we need to explicitly define okhttp3.ResponseBody with the error code and error body:

LibResponse response = new LibResponse(Status.ERROR.toString(), "Could not delete book for id : 1000");
ResponseBody respBody = ResponseBody.create(MediaType.parse("application/json"),
        new ObjectMapper().writeValueAsString(response));
Response<LibResponse> respLib = Response.error(500, respBody);
when(libraryClient.deleteBook(Long.valueOf("1000")))
        .thenReturn(Calls.response(respLib));

Conclusion

In this article, we introduced a Spring Boot REST client and REST server and looked at various capabilities of the Retrofit library. We took a closer look at the various components that need to be addressed to define a Retrofit client. Finally, we learned to mock the Retrofit client for unit tests. In conclusion, Retrofit along with OkHttp is an ideal library that works well with Spring and simplifies calls to a REST server.

Written By:

Ranjani Harish

Written By:

Ranjani Harish

Fun-loving, curious and enthusiastic IT developer who believes in constant learning.

Recent Posts

Handling Timezones in a Spring Boot Application

It is common to encounter applications that run in different time zones. Handling date and time operations consistently across multiple layers of an application can be tricky.

Read more

JUnit 5 by Examples

In software development, unit testing means writing tests to verify that our code is working as logical units. Different people tend to think of a “logical unit” in different ways.

Read more

Scheduling Jobs with Node.js

Have you ever wanted to perform a specific task on your application server at specific times without physically running them yourself?

Read more