0
votes

I want to handle any exception from feign client, even if service is not available. However I can not catch them using try/catch. This is my feign client:

@FeignClient(name = "api-service", url ="localhost:8888")
public interface ClientApi extends SomeApi {

}

Where api is:

@Path("/")
public interface SomeApi {

  @GET
  @Path("test")
  String getValueFromApi();

}

Usage of client with try/catch:

@Slf4j
@Service
@AllArgsConstructor
public class SampleController implements SomeApi {

  @Autowired
  private final ClientApi clientApi;

  @Override
  public String getValueFromApi() {
    try {
      return clientApi.getValueFromApi();
    } catch (Throwable e) {
      log.error("CAN'T CATCH");
      return "";
    }
  }
}

Dependencies are in versions:

  • spring-boot 2.2.2.RELEASE
  • spring-cloud Hoxton.SR1

Code should work according to How to manage Feign errors?.

I received few long stack traces among them exceptions are :

  1. Caused by: java.net.ConnectException: Connection refused (Connection refused)
  2. Caused by: feign.RetryableException: Connection refused (Connection refused) executing GET http://localhost:8888/test
  3. Caused by: com.netflix.hystrix.exception.HystrixRuntimeException: ClientApi#getValueFromApi() failed and no fallback available.

How to properly catch Feign exeptions, even if client service (in this case localhost:8888) is not available?

Ps. When feign client service is available it works, ok. I am just focused on the exceptions aspect.

2

2 Answers

0
votes

A better way to handle the situation where your service is not available is to use a circuit breaker pattern. Fortunately, it is easy using Netflix Hystrix as an implementation of the circuit breaker pattern.

First of all, you need to enable Hystrix for feign clients in application configuration.

application.yml

feign:
  hystrix:
    enabled: true

Then you should write a fallback class for the specified feign client interface. In this case getValueFormApi method in fallback class will act mostly like catch block that you wrote(with exception when circuit will be in open state and original method will not be attempted).

@Component
public class ClientApiFallback implements ClientApi {

  @Override
  public String getValueFromApi(){
    return "Catch from fallback";
  }
}

Lastly, you just need to specify the fallback class for your feign client.

@FeignClient(name = "api-service", url ="localhost:8888", fallback = ClientApiFallback.class)
public interface ClientApi extends SomeApi {

}

That way your method getValueFromApi is fail safe. If, for any reason, any uncaught exceptions escape from getValueFromApi the ClientApiFallback method will be called.

0
votes

To enable circuit breaker and also configure your application to deal with unexpected errors, you need to:

1.- Enable the circuit breaker itself

@SpringBootApplication
@EnableFeignClients("com.perritotutorials.feign.client")
@EnableCircuitBreaker
public class FeignDemoClientApplication {

2.- Create your fallback bean

@Slf4j
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PetAdoptionClientFallbackBean implements PetAdoptionClient {
@Setter
    private Throwable cause;
@Override
    public void savePet(@RequestBody Map<String, ?> pet) {
        log.error("You are on fallback interface!!! - ERROR: {}", cause);
    }
}

Some things you must keep in mind for fallback implementations:

  • Must be marked as @Component, they are unique across the application.
  • Fallback bean should have a Prototype scope because we want a new one to be created for each exception.
  • Use constructor injection for testing purposes.

3.- Your ErrorDecoder, to implement fallback startegies depending on the HTTP error returned:


public class MyErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() >= 400 && response.status() <= 499) {
            return new MyCustomBadRequestException();
        }
if (response.status() >= 500) {
            return new RetryableException();
        }
        return defaultErrorDecoder.decode(methodKey, response);
    }
}

4.- In your configuration class, add the Retryer and the ErrorDecoder into the Spring context:


@Bean
public MyErrorDecoder myErrorDecoder() {
  return new MyErrorDecoder();
}

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

You can also add customization to the Retryer:


class CustomRetryer implements Retryer {
private final int maxAttempts;
    private final long backoff;
    int attempt;
public CustomRetryer() {
        this(2000, 5); //5 times, each 2 seconds
    }
public CustomRetryer(long backoff, int maxAttempts) {
        this.backoff = backoff;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }
public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e; 
        }
try {
            Thread.sleep(backoff);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }
@Override
    public Retryer clone() {
        return new CustomRetryer(backoff, maxAttempts);
    }
}

If you want to get a functional example about how to implement Feign in your application, read this article.