REST Top

Get started with Spring 5 and Spring Boot 2, through the reference Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

Calling external services through the REST endpoint is a common activity that was made very straightforward by libraries like Feign. However, a lot of things can go wrong during such calls. Many of these problems are random or temporary.

In this tutorial, we'll learn how to retry failed calls and make more resilient REST clients.

2. Feign Client Setup

First, let's create a simple Feign client builder that we'll later enhance with retrying features. We'll use OkHttpClient as the HTTP client. Also, we'll use GsonEncoder and GsonDecoder for encoding and decoding the requests and the responses. Finally, we'll need to specify the target's URI and response type:

public class ResilientFeignClientBuilder {
    public static <T> T createClient(Class<T> type, String uri) {
        return Feign.builder()
          .client(new OkHttpClient())
          .encoder(new GsonEncoder())
          .decoder(new GsonDecoder())
          .target(type, uri);
    }
}

Alternatively, if we use Spring, we could let it auto-wire Feign client with available beans.

3. Feign Retryer

Fortunately, retrying abilities are baked in Feign, and they just need to be configured. We can do that by providing an implementation of the Retryer interface to the client builder.

Its most important method, continueOrPropagate, accepts RetryableException as an argument and returns nothing. Upon execution, it either throws an exception or exits successfully (usually after sleeping). If it doesn't throw an exception, Feign will continue to retry the call. If the exception is thrown, it'll be propagated and will effectively finish the call with an error.

3.1. Naive Implementation

Let's write a very simple implementation of Retryer that will always retry calls after waiting one second:

public class NaiveRetryer implements feign.Retryer {
    @Override
    public void continueOrPropagate(RetryableException e) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }
}

Because Retryer implements the Cloneable interface, we also needed to override the clone method.

@Override
public Retryer clone() {
    return new NaiveRetryer();
}

Finally, we need to add our implementation to client builder:

public static <T> T createClient(Class<T> type, String uri) {
    return Feign.builder()
      // ...
      .retryer(new NaiveRetryer())    
      // ...
}

Alternatively, if we are using Spring, we could annotate NaiveRetryer with @Component annotation or define a bean in the configuration class and let Spring do the rest of the work:

@Bean
public Retryer retryer() {
    return new NaiveRetryer();
}

3.2. Default Implementation

Feign provides a sensible default implementation of the Retryer interface. It'll retry only a given number of times, will start with some time interval, and then increase it with each retry up to provided maximum. Let's define it with starting interval of 100 milliseconds, the maximum interval of 3 seconds, and the maximum number of attempts of 5:

public static <T> T createClient(Class<T> type, String uri) {
    return Feign.builder()
// ...
      .retryer(new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5))    
// ...
}

3.3. No Retrying

If we don't want Feign to ever retry any calls, we can provide Retryer.NEVER_RETRY implementation to the client builder. It'll simply propagate the exception every time.

4. Creating Retryable Exceptions

In the previous section, we learned to control how often we retry calls. Now let's see how to control when we want to retry the call and when we want to simply throw the exception.

4.1. ErrorDecoder and RetryableException

When we receive an erroneous response, Feign passes it to an instance of the ErrorDecoder interface that decides what to do with it. Most importantly, the decoder can map an exception to an instance of RetryableException, enabling Retryer to retry the call. The default implementation of ErrorDecoder only creates a RetryableExeception instance when the response contains the “Retry-After” header. Most commonly, we can find it in 503 Service Unavailable responses.

That's good default behavior, but sometimes we need to be more flexible. For example, we could be communicating with an external service that, from time to time, randomly responds with 500 Internal Server Error, and we have no power to fix it. What we can do is to retry the call because we know that it'll probably work next time. To achieve that, we'll need to write a custom ErrorDecoder implementation.

4.2. Creating Custom Error Decoder

There is only one method that we need to implement in our custom decoder: decode. It accepts two arguments, a String method key, and a Response object. It returns an exception, should it be an instance of RetryableException or some other exception that depends on the implementation.

Our decode method will simply check if the response's status code is higher or equal to 500. If that's the case, it'll create RetryableException. If not, it will return basic FeignException created with the errorStatus factory function from the FeignException class:

public class Custom5xxErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        FeignException exception = feign.FeignException.errorStatus(methodKey, response);
        int status = response.status();
        if (status >= 500) {
            return new RetryableException(
              response.status(),
              exception.getMessage(),
              response.request().httpMethod(),
              exception,
              null,
              response.request());
        }
        return exception;
    }
}

Mind that in this case, we create and return the exception, not throw it.

Finally, we need to plug our decoder in the client builder:

public static <T> T createClient(Class<T> type, String uri) {
    return Feign.builder()
      // ...
      .errorDecoder(new Custom5xxErrorDecoder())
      // ...
}

5. Summary

In this article, we learned how to control the retry logic of the Feign library. We looked into the Retryer interface and how it can be used to manipulate the time and number of retry attempts. Then we created our ErrorDecoder to control which responses warrant retry.

As always, all code examples can be found over on GitHub.

REST bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course :

>> CHECK OUT THE COURSE
REST footer banner
Comments are closed on this article!