Over the last couple of years, I’ve been using Feign to invoke HTTP APIs, let it be external or internal. If you are not familiar with Feign, here’s a very brief intro. Feign is a declarative HTTP client. You define an interface, take some magical annotations and you have yourself a fully functioning client that you can use to communicate via HTTP.
Feign is a standalone library, anybody can use it on a project. It comes with its own annotations, types and configuration. Here’s an example client from the docs:
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}
Having a tool to define APIs like this is a great way to reduce application complexity. Think about writing the same thing with Apache HttpComponents. Let me give you an idea:
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
String owner = ...
String repo = ...
HttpGet request = new HttpGet("https://api.github.com/repos/"
+ owner + "/" + repo +"/contributors");
CloseableHttpResponse response = httpClient.execute(request);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
String result = EntityUtils.toString(entity);
Gson gson = new Gson();
List<Contributor> contributors = gson.fromJson(result,
new TypeToken<ArrayList<Contributor>>(){}.getType());
...
}
} finally {
response.close();
}
} finally {
httpClient.close();
}
I hope the difference is obvious. There’s tons of boilerplate code in the latter example.
Since Spring is one of the most used base frameworks in any Java project, there’s another level of abstraction that the Spring ecosystem provides. What if you don’t need to rely on the custom Feign annotations but you use the same Spring annotations just like when a controller is defined, e.g. @RequestMapping, @PathVariable and so on.
Well, Spring Cloud adds this capability to your application. The former example with Spring annotations looks the following:
@FeignClient("github")
interface GitHub {
@RequestMapping(value = "/repos/{owner}/{repo}/contributors", method = GET)
List<Contributor> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);
@RequestMapping(value = "/repos/{owner}/{repo}/issues", method = POST)
void createIssue(Issue issue, @PathVariable("owner") String owner, @PathVariable("repo") String repo);
}
Quite neat compared to the previous examples.
Although, there’s one downside to any abstraction. Or maybe downside is not even the best word to describe it, its rather a trade-off that we – engineers – often forget.
One of the points of an abstraction is to hide details from its users to ease development, which is absolutely spot on in case of Feign. However, the trade-off you are going to make is less control over that particular piece of code. For normal use-cases it’s often perfectly fine but as soon as you hit a wall and you need some specific behavior, you have to start digging. Where? I guess most of us just Google for some time, hoping that somebody has asked a similar question on Stackoverflow. Sometimes it’s just not the case, and you have to jump right into the code to figure out how to work-around the framework.
Error handling in Feign
Fortunately the framework creators have thought about having some way of reacting to errors during an API call. The ErrorDecoder interface is used for that purpose.
public interface ErrorDecoder {
public Exception decode(String methodKey, Response response);
}
Such an interface implementation can be tied to creating a particular Feign client. In the standard Feign world, you can specify it during the Builder calls, like:
Feign.builder() .decoder(new CustomDecoder())
So you can customize the Decoder you’d like to use on a per client basis, but not on a method basis. That’s just a limitation of the capabilities the library is providing.
How does this look in the Spring world? If you’d like to use an ErrorDecoder, you can just simply register it as a bean and the framework will automatically pick it up and assign it to every Feign client.
@Configuration
public class CustomFeignConfiguration {
@Bean
public CustomDecoder customDecoder() {
return new CustomDecoder();
}
}
Very neat, but since within an application there could be several Feign clients used, does it make sense to use a single decoder for all clients? Probably not. But even if you go one level deeper, you might need to have different error handling logic for different API calls within the same client.
The junior implementation
So if you were carefully reading, you might have noticed a parameter in the signature of the ErrorDecoder, methodKey. The methodKey is automatically generated by the Feign library whenever an error response is received from the downstream API. An example methodKey looks the following:
UserServiceClient#findById(UUID)
It starts with the Feign client class name, then a hash symbol and then the name of the method, followed by its parameter types within parentheses. The key generation can be found here: Feign#configKey
The first implementation one might think would be some kind of string magic on the methodKey:
@Override
public Exception decode(String methodKey, Response response) {
if (methodKey.startsWith("UserServiceClient")) {
// dosomething specific to UserServiceClient
} else if (...) {
...
}
}
Obviously this is going to work, but it won’t scale and as soon as you rename a class/method, you’ll end up in some functional problems in your application since the String renaming might be easily messed up (even though IDEs are very smart these days).
Testing is going to be hell with an implementation like this, cyclomatic complexity is going to increase with the number of clients and API methods. There must be a solution out there and somebody must have figured it out already.
Well, I thought the same, but so far I haven’t found anything on this.
The proper solution
First of all, this solution is aiming to address Spring only, when using the bare Feign client library, there are some minor tweaks required.
For a Spring Cloud based Feign client, you need to use the @FeignClient annotation on the interface like this:
@FeignClient(name = "user-service", url = "http://localhost:9002")
public interface UserServiceClient {
@GetMapping("/users/{id}")
UserResponse findById(@PathVariable("id") UUID id)
}
Pretty easy I’d say. So what happens when there’s an error, for example 404 returned by the API?
2020-09-29 20:31:29.510 ERROR 26412 --- [ctor-http-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [c04278db-1] 500 Server Error for HTTP GET "/users/d6535843-effe-4eb7-b9ff-1689420921a3" feign.FeignException$NotFound: [404] during [GET] to [http://localhost:9002/users/d6535843-effe-4eb7-b9ff-1689420921a3] [UserServiceClient#findById(UUID)]: []
As you can see, the original API I was calling resulted in a 500 Internal Server Error because the downstream user-service was responding with a 404. Not good.
So what can I do if I want to translate this 404 response into a UserNotFoundException within the service? And what if I’d like to do something else for another method within the same client?
Well, let’s create a generic ErrorDecoder that can defer the error handling to other classes, similarly how we do with a @ControllerAdvice and @ExceptionHandler class in the Sprint MVC world.
I’m going to use a custom annotation to mark methods or the client class which needs special treatment on its error handling:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleFeignError {
Class<? extends FeignHttpExceptionHandler> value();
}
FeignHttpExceptionHandler is a simple interface with a single method:
public interface FeignHttpExceptionHandler {
Exception handle(Response response);
}
The usage is going to look the following:
@FeignClient(name = "user-service", url = "http://localhost:9002")
public interface UserServiceClient {
@GetMapping("/users/{id}")
@HandleFeignError(UserServiceClientExceptionHandler.class)
UserResponse findById(@PathVariable("id") UUID id) throws UserNotFoundException;
}
The implementation for UserServiceClientExceptionHandler is very simple:
@Component
public class UserServiceClientExceptionHandler implements FeignHttpExceptionHandler {
@Override
public Exception handle(Response response) {
HttpStatus httpStatus = HttpStatus.resolve(response.status());
String body = FeignUtils.readBody(response.body());
if (HttpStatus.NOT_FOUND.equals(httpStatus)) {
return new UserNotFoundException(body);
}
return new RuntimeException(body);
}
}
Of course you can make it more sophisticated, this is just an example.
So how does the annotation work? As I said, we are going to use a special ErrorDecoder. First of all, we have to understand the signature of the ErrorDecoder interface. Since there’s no information within the decoder which client’s which method was called, somehow we have to figure it out so we can invoke the corresponding error handler.
One way to do it is to utilize the methodKey parameter and build a map based on that with the error handlers. But before that, we need to somehow get a reference to all the Feign clients registered within the application:
@Component
@RequiredArgsConstructor
public class ExceptionHandlingFeignErrorDecoder implements ErrorDecoder {
private final ApplicationContext applicationContext;
private final Map<String, FeignHttpExceptionHandler> exceptionHandlerMap = new HashMap<>();
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class);
List<Method> clientMethods = feignClients.values().stream()
.map(Object::getClass)
.map(aClass -> aClass.getInterfaces()[0])
.map(ReflectionUtils::getDeclaredMethods)
.flatMap(Arrays::stream)
.collect(Collectors.toList());
for (Method m : clientMethods) {
String configKey = Feign.configKey(m.getDeclaringClass(), m);
HandleFeignError handlerAnnotation = getHandleFeignErrorAnnotation(m);
if (handlerAnnotation != null) {
FeignHttpExceptionHandler handler = applicationContext.getBean(handlerAnnotation.value());
exceptionHandlerMap.put(configKey, handler);
}
}
}
private HandleFeignError getHandleFeignErrorAnnotation(Method m) {
HandleFeignError result = m.getAnnotation(HandleFeignError.class);
if (result == null) {
result = m.getDeclaringClass().getAnnotation(HandleFeignError.class);
}
return result;
}
}
First off, it’s loading all the Spring beans with the @FeignClient annotation. Since Feign is based on interfaces, there are JDK proxies involved, that’s why we need to call aClass.getInterfaces()[0] to get the actual interface with its methods.
Then the only trick is to calculate the methodKey which I’m doing using the Feign.configKey method call (that’s the one Feign is also using). And as well we’re searching for the HandleFeignError annotation on method and on class level in the same order.
So as soon as the Spring context is set up, we’ll have a map of methodKeys to actual exception handlers.
The second thing we need to do is to make sure this class implements the ErrorDecoder interface.
@Component
@RequiredArgsConstructor
public class ExceptionHandlingFeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder.Default defaultDecoder = new Default();
// rest is omitted for simplicity
@Override
public Exception decode(String methodKey, Response response) {
FeignHttpExceptionHandler handler = exceptionHandlerMap.get(methodKey);
if (handler != null) {
return handler.handle(response);
}
return defaultDecoder.decode(methodKey, response);
}
}
So the decode method is very simple. If the map contains the methodKey with its corresponding exception handler, we’ll use that for resolving the proper Exception, otherwise the solution is falling back to the Default ErrorDecoder.
Using the Feign client afterwards is quite easy:
try {
UserResponse userResponse = userServiceClient.findById(id);
// do something with the UserResponse
} catch (UserNotFoundException e) {
// Oops, the user is not found in the system
// let's do some error handling
}
Conclusion
With this solution in place, you can easily define proper error handling on the level of Feign client methods with any custom logic/custom exceptions you want to use. It allows you to build a more maintainable and robust application.
As usual, if you are interested in more, follow me on Twitter for updates.
UPDATE: the code can be found on my GitHub here.
I have some fiegn client to send request other micro service.
@FeignClient(name="userservice")
public interface UserClient {
@RequestMapping(
method= RequestMethod.GET,
path = "/userlist"
)
String getUserByid(@RequestParam(value ="id") String id);
}
Now I am sending request like this
try {
String responseData = userClient.getUserByid(id);
return responseData;
} catch(FeignException e) {
logger.error("Failed to get user", id);
} catch (Exception e) {
logger.error("Failed to get user", id);
}
Here the problem is if any FeignException happens I don’t get any error code.
I need to send a corresponding error codes in other APIS to send to caller
So how to extract the error code? I want to extract error code and build a responseEntity
I got this code but dont know how exactly I can use in my function.
aSemy
3,7342 gold badges22 silver badges41 bronze badges
asked Mar 6, 2019 at 10:05
I’m late to party but here are my 2 cents. We had same use case to handle exceptions based on error code and we used custom ErrorDecoder.
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
String requestUrl = response.request().url();
Response.Body responseBody = response.body();
HttpStatus responseStatus = HttpStatus.valueOf(response.status());
if (responseStatus.is5xxServerError()) {
return new RestApiServerException(requestUrl, responseBody);
} else if (responseStatus.is4xxClientError()) {
return new RestApiClientException(requestUrl, responseBody);
} else {
return new Exception("Generic exception");
}
}
}
Return @Bean of above class in FeignClientConfiguration class.
public class MyFeignClientConfiguration {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}
Use this as your config class for FeignClient.
@FeignClient(
value = "myFeignClient",
configuration = MyFeignClientConfiguration.class
)
Then you can handle these exceptions using GlobalExceptionHandler.
aSemy
3,7342 gold badges22 silver badges41 bronze badges
answered May 22, 2021 at 15:07
VaibSVaibS
1,4231 gold badge14 silver badges19 bronze badges
1
Not the same issue, but this helped in my situation.
OpenFeign’s FeignException doesn’t bind to a specific HTTP status (i.e. doesn’t use Spring’s @ResponseStatus annotation), which makes Spring default to 500 whenever faced with a FeignException. That’s okay because a FeignException can have numerous causes that can’t be related to a particular HTTP status.
However you can change the way that Spring handles FeignExceptions. Simply define an ExceptionHandler that handles the FeignException
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FeignException.class)
public String handleFeignStatusException(FeignException e, HttpServletResponse response) {
response.setStatus(e.status());
return "feignError";
}
}
This example makes Spring return the same HTTP status that you received
aSemy
3,7342 gold badges22 silver badges41 bronze badges
answered May 21, 2019 at 14:59
SrinathSrinath
1571 silver badge3 bronze badges
5
did you try to implement FallbackFactory on your feign client ?
https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html#spring-cloud-feign-hystrix-fallback
On the create method, before return, you can retrieve the http status code with this snippet :
String httpStatus = cause instanceof FeignException ? Integer.toString(((FeignException) cause).status()) : "";
Exemple :
@FeignClient(name="userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@RequestMapping(
method= RequestMethod.GET,
path = "/userlist")
String getUserByid(@RequestParam(value ="id") String id);
}
@Component
static class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
String httpStatus = cause instanceof FeignException ? Integer.toString(((FeignException) cause).status()) : "";
return new UserClient() {
@Override
public String getUserByid() {
logger.error(httpStatus);
// what you want to answer back (logger, exception catch by a ControllerAdvice, etc)
}
};
}
}
ikhvjs
4,8742 gold badges9 silver badges31 bronze badges
answered Mar 6, 2019 at 22:59
![]()
rphlmrrphlmr
6684 silver badges11 bronze badges
2
I’m kinda late too, but I wanted to make sure you check out other potential solutions as well.
Feign with Spring has a notoriously bad way of handling exceptions so I came up with a custom solution that creates this robust environment to define your business exceptions the way you want.
It’s utilizing a custom ErrorDecoder registered to the Feign clients and adds the possibility to customize your exception handling based on method or class level.
Check it out: Maintainable error handling with Feign clients? Not a dream anymore
answered Dec 3, 2021 at 19:57
Arnold GalovicsArnold Galovics
3,2043 gold badges18 silver badges29 bronze badges
1
There’s a ErrorDecored interface for that like it says in the documentation
The answer above about FallbackFactory is viable but falls into some other layer of abstraction.
![]()
leaqui
5036 silver badges21 bronze badges
answered Aug 26, 2020 at 13:54
1
Tuesday. October 16, 2018 — 10 mins
Данная статья является частью статей об использование Spring Cloud #Про Spring Cloud.
Немного теории
Feign — это декларативный HTTP клиент, разработанный компанией Netflix. Основным преиммуществом решения является то, что разработчику необходимо только описать (декларировать и аннотировать) интерфейс, в то время как фактическая реализация будет создана во время выполнения. Feign поддерживает подключаемые аннотации, включая аннотации JAX-RS и Spring MVC (дополнительно к аннотациям самого Feign)
Ribbon также является детищем компании Netflix и отвечает за межпроцессную коммуникацию (Inter Process Communication — IPC). Основная бизнес-задача Ribbon — это организация клиентских алгоритмов балансировки (client-side load balancing). Дополнительные возможности, которые стоит отметить: интеграция с Service Discovery (из коробки есть поддержка Eureka), поддержка паттерна Fault Tolerance (Ribbon понимает и динамически определяет состояние сервисов), поддержка правил балансировки (по умолчанию используется алгоритм Round Robin).
Spring Cloud интегрирует в Feign Ribbon и Eureka для клиента микросервисной архитектуры с возможностями балансировки.
Миграция на Feign в приложении AC Backup
Приведем пример миграции на основе клиента сервиса к user-service.
Для начала в проекте user-client подключим новые зависимости на Feign и Ribbon:
dependencies {
// ...
compile "org.springframework.cloud:spring-cloud-starter-openfeign:2.0.2.RELEASE"
// ...
}
Далее мы аннотируем интерфейс UserResource аннотациями Spring MVC, которые будут декларировать наш формат взаимодействия. Такой результат мы получаем:
@RequestMapping(path = "/user-service")
public interface UserResource {
@GetMapping(path = "/client/{id}")
ResponseEntity<Client> getUserInfo(@PathVariable("id") Long id);
}
Следующий шаг, чтобы отдельно не описывать интерфейс Feign и реализацию в виде RestController, я рекомендую наследовать контроллер от нашего интерфейса, в итоге у нас получается вот такая реализация контроллера:
@RestController
public class UserController implements UserResource {
private final ClientService clientService;
public UserController(ClientService clientService) {
this.clientService = clientService;
}
public ResponseEntity<Client> getUserInfo(@PathVariable("id") Long id) {
Client result = clientService.getClientInfo(id);
if (result == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} else {
return new ResponseEntity<>(result, HttpStatus.OK);
}
}
}
И последний шаг для описания нашего интерфейса для взаимодействия с использованием Feign, необходимо добавить аннотации, указывающие, что данный интерфейс будет использоваться для генерации клиента Feign:
package com.balynsky.ac.user.clients;
import com.balynsky.ac.user.UserResource;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(value = "user", decode404 = true)
public interface UserClient extends UserResource {
}
В результате мы имеем полностью подготовленную библиотеку для создания клиента с помощью Feign.
Я рекомендую выделять данную настройку в отдельный интерфейс, как показано в примере: во первых мы отделяем контекст для клиента Feign от интерфейса описывающего поведение, а во вторых мы можем дополнительно описывать конфигурацию Feign для данного клиента внутри интерфейса (пример такой конфигурации будет в заключительной части данной статьи)
Обращаю внимание, что для возвращаемого параметра в интерфейсе мы используем ResponseEntity. Это решение выбрано сознательно, для того, чтобы в последствии можно было получать и обрабатывать не только содержимое ответа, но и параметры из заголовков. Такие как HTTP код возврата ошибки
Последнее, что нам остается — это удалить “устаревшую” реализацию клиента сервиса, а именно класс ClientResourceImpl.java
Настройка Ribbon для подключения к сервисам приложения
Для начала работы с использованием Ribbon в проект backup-service мы добавляем зависимость для Ribbon:
dependencies {
// ...
compile "org.springframework.cloud:spring-cloud-starter-netflix-ribbon:2.0.2.RELEASE"
// ...
}
Далее, мы должны сконфигурировать Ribbon, указав ему, где непосредственно находится продюсер сервиса, для этого в файле конфигурации backup-service.yml необходимо добавить следующие настройки:
user: //Название сервиса из FeignClient
ribbon:
eureka:
enabled: false // отключаем использование ServiceGateway для данного клиента
listOfServers: localhost:9081 // Сервер, где находится продюсер
Если бы мы не использовали Feign для генерации клиента, то использование Ribbon отдельно от Feign выглядело бы так:
@RibbonClient(name = "user",configuration = RibbonConfiguration.class)
public class ServerLocationApp {
@LoadBalanced
@Bean
RestTemplate getRestTemplate() {
return new RestTemplate();
}
@Autowired
RestTemplate restTemplate;
public Client getUserInfo(Long id) {
return this.restTemplate.getForObject("http://localhost:9081/user-service/client/" + id, Client.class);
}
}
Где конфигурация может быть описана следующим файлом:
public class RibbonConfiguration {
@Autowired
IClientConfig ribbonClientConfig;
@Bean
public IPing ribbonPing(IClientConfig config) {
return new PingUrl();
}
@Bean
public IRule ribbonRule(IClientConfig config) {
return new WeightedResponseTimeRule();
}
}
В конфигурации можно настроить следующие параметры:
- Rule – Описание правила балансировки нагрузки для приложения
- Ping – Механизм определения доступности сервиса
- ServerList – Список серверов для доступа к сервису. Может быть как статическим, так и динамическим.
В нашем примере, ServerList не был настроен (поэтому мы используем полный путь для RestTemplate), а для примера правило балансировки выбрано WeightedResponseTimeRule, что означает для этого правила каждому серверу присваивается вес в соответствии с его средним временем отклика. Чем дольше время отклика, тем меньше веса он получит. Правило случайно выбирает сервер, где вероятность определяется весом сервера.
Детальнее прочитать о Ribbon можно по ссылке
Возврат ошибок (ResponseEntity vs Exception)
В начале статьи мы рассмотрели один из способов, через который можно возвращать ошибку потребителю сервиса, а именно с использование ResponseEntity. Данный класс является обверткой над стандартным ответом и позволяет получить HTTP код ответа.
Вторым, не менее популярным способом, является передача ошибки через Exceptions. Таким образом продюсер сервиса генерирует исключение, которое в дальнейшем сереализуется и отправляется клиенту отдельной моделью. Для сериализации exceptions есть несколько вариантов, которые описаны по ссылке.
Для нашего примера мы будем использовать RestControllerAdvice, для глобального отслеживания исключений.
Для начала добавим новый проект feign-error-decoder к нашему приложению, куда выделим классы, отвечающие за обработку данных исключений.
Для начала нам понадобится базовая модель исключения ServiceException, от которой будут наследовать все остальные исключения.
@Data
@JsonIgnoreProperties(value = {"stackTrace", "localizedMessage", "suppressed", "cause"})
public abstract class ServiceException extends Exception {
@NonNull
private final String errorCode;
public ServiceException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public ServiceException(String message, Throwable cause, String errorCode) {
super(message, cause);
this.errorCode = errorCode;
}
}
Далее нам понадобится создать класс, который будет обрабатывать ошибки для Feign клиента и генерировать непосредственно сами сообщения на клиенте. Для этого нам мы используем решение, которое детально описано по ссылке. Для нас наибольшую ценность представляет класс FeignServiceExceptionErrorDecoder.
Необходимые классы мы размещаем в проекте feign-error-decoder.
Дополнительно нам понадобится ControllerAdvice, который на сервере будет отвечать за правильную сериализацию исключения, его код представлен ниже:
@RestControllerAdvice
public class ServiceExceptionHandlerAdvice {
@ExceptionHandler({ServiceException.class})
ResponseEntity<ServiceException> handle(ServiceException exception) {
ResponseStatus status = exception.getClass().getAnnotation(ResponseStatus.class);
return new ResponseEntity<ServiceException>(exception, status == null ? HttpStatus.BAD_REQUEST : status.code());
}
}
Чтобы научить клиента Feign использовать данный класс, необходимо прописать FeignServiceExceptionErrorDecoder как обработчик ошибок. Для этого мы модифицируем StorageClient следующим образом:
@FeignClient(value = "storage", configuration = StorageClient.StorageClientConfiguration.class)
public interface StorageClient extends StorageResource {
class StorageClientConfiguration {
@Bean
public ErrorDecoder provideErrorDecoder() throws Exception {
return new FeignServiceExceptionErrorDecoder(StorageClient.class);
}
}
}
Как мы видим, в аннотации мы добавили класс для конфигурирования клиента. В рамках данного класса мы создаем необходимый нам ErrorDecoder.
В некоторых примерах конфигурацию Feign (в нашем примере StorageClientConfiguration) помечена аннотацией спринга @Configuration. Данная конфигурация не является обязательной, согласно официальной документации. Но может привести к side-effect, когда один ErrorDecoder будет применен для всех клиентов Feign. Для некоторых случаев это оправдано, но наш случай требуется отдельного ErrorDecoder для каждого интерфейса.
Теперь можно приступить к созданию первого исключения BadRequestException:
@EqualsAndHashCode(callSuper = true)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends ServiceException implements Serializable {
public BadRequestException(final String message) {
super(message, "BAD_REQUEST_EXCEPTION");
}
}
а декларация сервиса сервиса соответственно будет выглядеть так:
@RequestMapping(value = "/storage-service")
public interface StorageResource {
@RequestMapping(value = "storage", method = RequestMethod.POST)
SoulEntity saveSoul(@RequestBody SoulEntity soul) throws BadRequestException;
}
Теперь у нас возможно возврат ошибки через механизм исключений. Это позволяет клиенту сервиса ловить данные исключения и обрабатывать, будто бы мы используем обычное написание кода на java.
Использование клиентов Feign в приложении backup-service
Для начала использования клиентов Feign необходимо использовать аннотацию, которая включает данный функционал, а на вход ей передать список пакетов, в которых находятся клиенты Feign (только для нашего случая). Для этого создадим класс FeignRibbonConfig в пакете config:
@Configuration
@EnableFeignClients(basePackages = {"com.balynsky.ac.storage.clients", "com.balynsky.ac.user.clients"})
public class FeignRibbonConfig {
}
Далее для использования в нашем сервисе BackupServiceImpl необходимо заменить типы входных параметров. А именно: UserResource заменить на UserClient, а StorageResource на StorageClient
Давайте используем RestClient из IntellijIdea
Запрос:
POST http://localhost:9080/backup-service/backup
Accept: */*
Content-Type: application/json
Cache-Control: no-cache
{
"clientId": 1,
"body": "body"
}
Ответ:
POST http://localhost:9080/backup-service/backup
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 04 Dec 2018 14:52:05 GMT
{
"id": 2,
"clientId": 1,
"body": "body"
}
Response code: 201; Time: 298ms; Content length: 35 bytes
Итоги:
В рамках этой статьи, мы научили наше приложение коммуницировать между разными микросервисами используя Feign и Ribbon.
Проект опубликован в репозитории на GitHub
By default Feign only throws FeignException for any error situation, but you probably want an application specific exception instead. This is easily done by providing your own implementation of feign.codec.ErrorDecoder to Feign.builder.errorDecoder().
An example of such an ErrorDecoder implementation could be as simple as:
public class StashErrorDecoder implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { if (response.status() >= 400 && response.status() <= 499) { return new StashClientException( response.status(), response.reason() ); } if (response.status() >= 500 && response.status() <= 599) { return new StashServerException( response.status(), response.reason() ); } return errorStatus(methodKey, response); } }
Which you can then provide in your Feign.builder() like so:
return Feign.builder() .errorDecoder(new StashErrorDecoder()) .target(StashApi.class, url);
In this tutorial, I will share with you how you can use Feign ErrorDecoder to handle errors that occur when using Feign client in Microservices communication.
For step-by-step video beginner lessons demonstrating how to do Feign error handling and how to build Microservices with Spring Boot and Spring Cloud, have a look at this page: Spring Boot Microservices and Spring Cloud.
Setup Feign
To make sure your Feign client works well and the errors you are getting are not caused by an incorrect setup of your Feign client, please have a look at the following tutorial to learn how to add Feign to your Spring Boot project and make it work: Feign Client to Call Another Microservice.
Create ErrorDecoder
To be able to use ErrorDecoder, you will need to create a new Java class and make it implement ErrorDecoder interface. Implementing ErrorDecoder interface gives you access to Method Key and Response objects.
- methodKey – will contain a Feign client class name and a method name,
- Response – will allow you to access the HTTP status code, the Body of HTTP Response and the Request object. You can use these details when handling an error message and preparing a response.
Below is an example of ErrorDecoder interface being implemented.
@Component
public class FeignErrorDecoder implements ErrorDecoder {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()){
case 400:
logger.error("Status code " + response.status() + ", methodKey = " + methodKey);
case 404:
{
logger.error("Error took place when using Feign client to send HTTP Request. Status code " + response.status() + ", methodKey = " + methodKey);
return new ResponseStatusException(HttpStatus.valueOf(response.status()), "<You can add error message description here>");
}
default:
return new Exception(response.reason());
}
}
}
Please note the use of @Component annotation. If not using @Component annotation, you can also create Feign ErrorDecoder as a @Bean the following way:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class PhotoAppApiApplication {
public static void main(String[] args) {
SpringApplication.run(PhotoAppApiApplication.class, args);
}
@Bean
public FeignErrorDecoder errorDecoder() {
return new FeignErrorDecoder();
}
}
Now that your Feign ErrorDecoder interface is implemented, you can try using Feign client to send HTTP Request to a Web Service endpoint that does not exist and see if the 404 HTTP Status code is handled. You get a correct switch case executed, and the error message is logged.
Feign Client HTTP Requests Logging
When working with Feign clients, it is also very helpful to enable HTTP Requests logging. Read the following tutorial to learn how to enable Feign logging in your Spring Boot application:
- Feign HTTP Requests Logging
If you need to see how it is all done in step-by-step video lessons, checkout out at this page: Spring Boot Microservices and Spring Cloud.
I hope this tutorial was helpful to you.
There are many very good online video courses that teach how to build Spring Boot Microservices with Spring Cloud. Have a look at the list below and see if you like any of them.
Introduction
Feign is a declarative web service client. It makes the client implementation process fast. You can simply define a Java interface with a readable method names and annotations, and make it a functioning web client. You can refer to the readme[1] to have the basic knowledge on Feign. Also there are ample of blogs that you can refer. Through this post, I am going to explain on how you can achieve Error Decoding, and Retrying functionality in a Feign client.
Setting up dependencies
This post uses spring-cloud-starter Hoxton.RELEASE version of spring cloud. In the pom file, you need to add the spring-cloud-starter-parent as the parent-pom file and spring-cloud-dependencies as the dependency management. Spring-cloud-dependencies provide the spring-cloud dependency versions according to the parent pom version. Thereafter you need to add the following dependencies for the rest of implementation:
- spring-boot-starter
- spring-boot-starter-web
- spring-cloud-starter-openfeign
It’s worth mentioning that feign was created and maintained by Netflix OSS and currently maintained separately from Nextflix. Once you wire-up all dependencies, the final pom file would look like below:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Hoxton.RELEASE</version>
</parent>
<groupId>com.buddhima.testing</groupId>
<artifactId>acn-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>acn-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Enable feign clients and define a feign client
To enable feign clients, you need to use @EnableFeignClients annotation in the main class definition. Then you can simply create an interface to the external web-services. In this post I’m not going to talk about the annotations because you can find a good documentation here [1][2]. Following is a sample interface I created for this post:
DrmServiceClient.java
package com.buddhima.testing.demo.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
@FeignClient(value="drmClient", url="http://www.mocky.io/v2/5e2d966a3000006200e77d2d") // to produce 400 HTTP status
public interface DrmServiceClient {
@RequestMapping(method = RequestMethod.GET, value = "/posts")
List<Object> getObjects();
}
Enabling logging for feign client
First I decided to talk about logging as this helps to demonstrate the behaviors in next steps.
To enable extended logging for feign clients, you need to follow two steps.
- Enabling DEBUG log-level for feign client
- Change feign client log-level (valid values are NONE, BASIC, HEADERS, FULL)
application.yml
logging:
level:
com.buddhima.testing.demo.client: DEBUG
feign:
client:
config:
default:
loggerLevel: BASIC
After this configuration, you can view Request-Response logs in the microservice log. You can further increase logging by changing the logger-level to HEADER or FULL.
Throughout this post, I am discussing about configuring the feign client through application.yml file. But there are multiple ways of doing the same.
Error Decoder for Feign client
You can use error-decoder to act based on the erroneous HTTP responses. I have observed that error-decoder does not get triggered on success scenarios. To implement an error-decoder, you need to implement a class using ErrorDecoder interface and add that in the configuration.
DrmClientErrorDecoder.java
package com.buddhima.testing.demo.client;
import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;
public class DrmClientErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String s, Response response) {
System.out.println("Error Response!!!");
if (400 == response.status()) {
System.out.println("It's a 400 Error!!!");
}
return defaultErrorDecoder.decode(s, response);
}
}
application.yml
logging:
level:
com.buddhima.testing.demo.client: DEBUG
feign:
client:
config:
default:
errorDecoder: com.buddhima.testing.demo.client.DrmClientErrorDecoder
loggerLevel: BASIC
Retryer for Feign client
Retryer could be a useful entity to retry your request in case of failure (network failures by default). You can configure default retryer with parameters by extending Retryer.Default class.
DrmClientRetryer.java
package com.buddhima.testing.demo.client;
import feign.Retryer;
public class DrmClientRetryer extends Retryer.Default {
public DrmClientRetryer() {
super();
}
}
application.yml
feign:
client:
config:
default:
errorDecoder: com.buddhima.testing.demo.client.DrmClientErrorDecoder
loggerLevel: BASIC
retryer: com.buddhima.testing.demo.client.DrmClientRetryer
Furthermore, if you need to retry based on HTTP status, you can throw RetryableException at error-decoder. Same goes, if you want to retry based on HTTP-headers.
DrmClientErrorDecoder.java
package com.buddhima.testing.demo.client;
import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;
public class DrmClientErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String s, Response response) {
System.out.println("Error Response!!!");
if (400 == response.status()) {
return new RetryableException(400, response.reason(), response.request().httpMethod(), null, response.request());
}
return defaultErrorDecoder.decode(s, response);
}
}
Following is a sample log, which demonstrate re-trying 5 times before giving-up.
2020-01-27 23:20:41.317 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:42.153 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (836ms) Error Response!!! 2020-01-27 23:20:42.317 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:42.318 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:42.551 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (231ms) Error Response!!! 2020-01-27 23:20:42.776 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:42.777 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:43.062 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (285ms) Error Response!!! 2020-01-27 23:20:43.400 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:43.400 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:43.678 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (276ms) Error Response!!! 2020-01-27 23:20:44.185 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:44.186 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:44.395 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (208ms)
In the above log, you can see that same request attempted 5 times back-to-back before it was declared as a failure. You can change the default values through the Retryer implementation to desired. Furthermore, you can alter the error-decoder to refer specific HTTP headers (eg: Retry-After header).
Conclusion
Through this post, I discussed on Feign clients with two different use-cases. First you have seen, how to react based on HTTP error statuses and later about retrying requests. I hope this will be beneficial for your spring boot application development.
I have uploaded the source-code used in this post at here: https://github.com/Buddhima/feign-demo
References
[1] https://github.com/OpenFeign/feign/blob/master/README.md
[2] https://cloud.spring.io/spring-cloud-openfeign/reference/html/
[3] http://www.matez.de/index.php/2017/04/12/exploring-feign-retrying/
OpenFeign’s FeignException doesn’t bind to a specific HTTP status (i.e. doesn’t use Spring’s @ResponseStatus annotation), which makes Spring default to 500 whenever faced with a FeignException.
Sample response when FeignException occurs
{
"timestamp": "2019-07-28T08:53:12.168+0000",
"status": 500,
"error": "Internal Server Error",
"message": "status 400 reading client#sendMessage(MessagePayload)",
"path": "/send-notification"
}
That’s because the root cause for FeignException may not be even related to Http status code sent by remote web service.
In order to propagate the actual error message to the client, we can write our custom ExceptionHandler using a ControllerAdvice.
FeignExceptionHandler.java
import feign.FeignException;
import org.json.JSONObject;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.Map;
@RestControllerAdvice
public class FeignExceptionHandler {
@ExceptionHandler(FeignException.BadRequest.class) (1)
public Map<String, Object> handleFeignStatusException(FeignException e, HttpServletResponse response) {
response.setStatus(e.status());
return new JSONObject(e.contentUTF8()).toMap(); (2)
}
}
| 1 | Handling FeignException for BadRequests only. We can write similar ExceptionHandlers for other types of exceptions. |
| 2 | We are assuming here that remote web service returns a JSON response for error messages |
After we add this ExceptionHandler, we will be able to see custom exception response with additional details from remote web service.
Propagating actual error message to the caller from remote web service
{
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"path": "/api/message/notification/",
"error_code": 2151,
"message": "Target id not valid",
"status": 400,
"timestamp": 1564304731547
}
Feign, Hystrix, Ribbon, Eureka, are great tools, all nicely packed in Spring Cloud, allowing us to achieve great resilience in our massively distributed applications, with such great ease!!! This is true, at least till the easy part… To be honest, it is easier to get all the great resilience patterns working together with those tools than without, but making everything work as intended needs some studying, time and testing.
Unfortunately (or not) I’m not going to explain how to set all this up here, I’ll just point out some tricks with error management with those tools. I chose this topic because I’ve struggled a lot with this (really)!!!
If you are looking for a getting started tutorial on those tools I recommend the following articles:
- Feign, encore un client HTTP ? (French)
- The Spring Cloud documentation
- The source code because we always end up there…
There will be code in this article, but not that much, you can find the missing parts in this repository
Dependencies
Let’s say, after some trouble, you ended up with a dependency set looking like this one:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
Ok, so you are aiming at the full set:
- Of course, you are going to use
Eureka clientto get your service instances from yourEureka server - So
Ribboncan provide a proper client-side load-balancer using service names and not URLs (and decorateRestTemplateto use names and load-balancing) - Then comes
Hystrixwith lots of built-in anti-fragile patterns, another awesome tool but you need to keep an eye on it (not part of this article…) - Finally, everything is packed up by
Feignfor really easy-to-write rest clients
This article uses the following versions of Spring Cloud:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.13.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Configuration
These tools need configuration, let’s assume you have configured up something similar in your application.yml:
spring:
application:
name: my-awesome-app
eureka:
client:
serviceUrl:
defaultZone: http://my-eureka-instance:port/eureka/
feign:
hystrix:
enabled: true
hystrix:
threadpool:
default:
coreSize: 15
command:
default:
execution:
isolation:
strategy: THREAD
thread:
timeoutInMilliseconds: 2000
ribbon:
ReadTimeout: 400
ConnectTimeout: 100
OkToRetryOnAllOperations: true
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 1
This configuration will work if your application can register to Eureka using its hostname and application port. For production / cloud / any environment with proxies you need to have additional properties:
eureka.instance.hostnamewith the real hostname to use to reach your serviceeureka.instance.nonSecurePortwith the non-secure-port to use oreureka.instance.securePortwitheureka.instance.securePortEnabled=true
Also this configuration isn’t authenticated, it can be a good idea to add authentication to Eureka, depending on your network.
From the Ribbon configuration I see you have confidence in your Web Services, 400ms for a ReadTimeout is quite short, the shorter the better!
We can also notice that all your services are idempotent because you accept to have 4 calls instead of 1 if your network / servers starts to get messy (yes, this Ribbon configuration will make 4 requests if the response times out because it is actually doing: ( 1 + MaxAutoRetries ) x ( 1 + MaxAutoRetriesNextServer) = 4. So if you set 2 and 3 respectively, you will have up to 12 requests only from Ribbon).
This gets us to the 2000ms Hystrix timeout, a shorter value will result in requests being done without the application waiting for the result so this seems legit (due to ribbon configuration : (400 + 100) * 4).
Customization
Everything goes well, you quickly understand that, for all FeignClients without fallback you only get HystrixRuntimeException for any error. This exception is mainly saying that something went wrong and you don’t have a fallback but the cause can tell you a little bit more. You quickly build an ExceptionHandler to display nicer messages to users (because you don’t want to put fallbacks on all FeignClient).
One day you call a new external service and this service can have normal responses with HTTP 404 for some resources, so you add decode404 = true to your @FeignClient to get a response and avoid circuit breaking on those (if this option is not set, a 404 will be counted for circuit breaking). But you don’t get responses, what you get is:
...
Caused by: feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class ...
...
This is because the 404 from this service has a different form than «normal» responses (can be a simple String saying that the resource wasn’t found). A cool idea here would be to allow Optional<?> and ResponseEntity<?> types in FeignClient to get an empty body for those 404s.
AutoConfigured Spring Cloud Feign can map to ResponseEntity<?> but will fail to deserialize incompatible objects. It cannot, by default, put results in Optional<?> so it is still a cool feature to implement.
One way to achieve this is to define a Decoder similar to this:
package fr.ippon.feign;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import feign.FeignException;
import feign.Response;
import feign.Util;
import feign.codec.DecodeException;
import feign.codec.Decoder;
public class NotFoundAwareDecoder implements Decoder {
private final Decoder delegate;
public NotFoundAwareDecoder(Decoder delegate) {
Assert.notNull(delegate, "Can't build this decoder with a null delegated decoder");
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
if (!(type instanceof ParameterizedType)) {
return delegate.decode(response, type);
}
if (isParameterizedTypeOf(type, Optional.class)) {
return decodeOptional(response, type);
}
if (isParameterizedTypeOf(type, ResponseEntity.class)) {
return decodeResponseEntity(response, type);
}
return delegate.decode(response, type);
}
private boolean isParameterizedTypeOf(Type type, Class<?> clazz) {
ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getRawType().equals(clazz);
}
private Object decodeOptional(Response response, Type type) throws IOException {
if (response.status() == 404) {
return Optional.empty();
}
Type enclosedType = Util.resolveLastTypeParameter(type, Optional.class);
Object decodedValue = delegate.decode(response, enclosedType);
if (decodedValue == null) {
return Optional.empty();
}
return Optional.of(decodedValue);
}
private Object decodeResponseEntity(Response response, Type type) throws IOException {
if (response.status() == 404) {
return ResponseEntity.notFound().build();
}
return delegate.decode(response, type);
}
}
Then, a @Configuration file:
package fr.ippon.feign;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import feign.codec.Decoder;
@Configuration
@EnableCircuitBreaker
@EnableDiscoveryClient
public class FeignConfiguration {
@Bean
public Decoder notFoundAwareDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
return new NotFoundAwareDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters)));
}
}
Of course it is up to you to fit it to your exact needs, but this way you will be able to get proper responses.
Integration testing
All this really cool stuff can change from Spring Cloud one minor version to another (eg : Hystrix enabled by default to Hystrix disabled by default) so unless you aren’t missing any update (I don’t think it is possible) I strongly recommend adding good integration tests for this stack usage (unit tests will not be of any help here).
But having integration testing for this stack can be quite complicated. If we want to be as close as possible to reality we need:
- A running
Eurekainstance. - A running service registered on
Eureka. - A running client using this service.
One way to do this is to set up a dynamic test environment with Eureka and some applications but, depending on your organization, this can be really hard to achieve. Another way is to start all this in a Single JVM managed by JUnit thus integration with any build tool and CI platform will be really easy.
The drawback of this can be strange behaviors due to the Spring auto-configuration mechanism, it’s up to you to choose to make it in containers or this way, depending on what you can do.
To achieve this we will need to solve:
- The fact that we cannot use the native SpringTest class because it can only manage one application by default. We can work around this, by using
SpringApplication.run(...)and play with the resultingConfigurableApplicationContext. - The need to start on available ports. Simply add
--server.portinSpringApplication.run(...)withSocketUtils.findAvailableTcpPort(), not even a problem. - The impossibility to use any kind of default configuration path unless we want all our apps to get this configuration. This one is also easy, just add
--spring.config.locationwith a specific configuration in ourSpringApplication.run(...)and we can have separate configurations. - The need for our applications to have configurations depending on the
Eurekaserver port. For this one we will need to ensure thatEurekais the first one to start (not needed for production, our client can handle this very well but will be annoying for tests) and then give theEurekaport one way or another to the other applications. - The fact that we can’t, by default, start multiple Spring Boot applications on the same JVM instance because of JMX mbean name. Let’s disable it using
--spring.jmx.enabled=false(or change the default domain using--spring.jmx.default-domainwith a different name) and we are OK. - Finally, a strange one, you know that Spring Cloud tools use
Archaiusto manage their configuration, not the default Spring configuration system.Archaiustakes Spring Boot configuration into account when the first application starts on the JVM, for the next one they aren’t taken into account at the moment I’m writing this (check ArchaiusAutoConfiguration.configureArchaius(…) there is a staticAtomicBooleanused to ensure that the configuration isn’t loaded twice and «else» there is a TODO and a warn log). For our tests we will go for an ugly fix for this, reloading this configuration in anApplicationListener<ApplicationReadyEvent>will do the trick.
I have done this here using mainly JUnitRules to handle the applications parts, feel free to take it if you like it and adapt those tests to your needs.
At the time of this writing, the project takes ~45sec to build, which is very slow considering that most of this time is for integration tests on already battle tested code… but I really don’t want to miss a breaking change in my usage of this great stack so I consider this time to be fair enough.
If you don’t need it remove the part testing circuit breaking on all HTTP error codes since those tests are very slow due to the sleeping phase…
Once again, really take the time to make strong integration tests on your usage of this stack to avoid really bad surprises after some months!!!
Going further
Depending on what you want to build, what we have here can be more than enough on the application side but if you are planning to use this in the real world, you really need some good metrics and alerts (at least to keep an eye on your fallbacks and circuit breaker openings).
For this you can check Hystrix dashboard and Turbine to provide you with lots of useful metrics to get dashboards with lots of those:

You will then need to bind it to your alerting system, this will need some work and you are going to need to handle LOTS of data since those tools are really verbose (if you want to persist that data pay attention to your eviction strategy and choose a solid enough timeseries infrastructure). Depending on your needs and organization tools a simple metrics Counter on your fallbacks can do a good job. Once set up in your applications this will only need a @Counted(...) on your fallbacks methods.
It is also possible that the few tools discussed here are not antifragile enough for your needs, in that case, you can start by checking:
- Hystrix configurations you will see that there are plenty of things you can do (playing with circuit breaker configuration can really help in some cases). Don’t forget to add integration tests to ensure that the configuration you are adding is really behaving as expected.
- Feign retries: I totally skipped this part but there is a built-in retry mechanism in Spring Cloud Feign coming on top of Hystrix and Ribbon mechanisms. You can check
Retryer.Defaultto see the default retry strategy but this is kind of misleading in two ways:- First: if you have Hystrix Feign enabled the default retrier is
Retryer.NEVER_RETRY(check FeignClientsConfiguration.feignRetryer()) - Second : even if you define a
RetryerBean toRetryer.Defaultyou won’t get feign level retries by default because it is also important to checkErrorDecoder.Defaultto see that we have aRetryableExceptiononly when there is a well formatted date in theRetry-AfterHTTP header.
So if you want to play with this you will need to : - define an
ErrorDecoderthat ends up inRetryableExceptionin the cases you want (or add theRetry-Afterheader in your services). - change the
Retryerto the one actually retrying. - probably redefine the
Feign.BuilderBean (be careful to keep the@Scope("prototype")) to suit your needs.
- First: if you have Hystrix Feign enabled the default retrier is
So, do we go live?
This stack really is great and every developer using it daily will enjoy it, at least after one guy in the team spends days setting all this up to check some “vital” points :
- avoid retries on POST, PATCH and any non-idempotent services (should follow this)
- ensure that fallback calls and opened circuit breakers are tracked and explained
- ensure that
Eurekais secured and not a SPOF (even withoutEurekaup and running the apps can talk to each other, at least for a fair amount of time) - ensure that some minor version change will not silently break all this anti-fragile stuff with strong integration tests.
In my opinion, this is a really great stack that needs a lot of work and understanding. So, make sure to use it only if you need it and otherwise stick to RestTemplate until you have time to give it a good try!