26

I'm using Netflix Feign to call to one operation of a Microservice A to other other operation of a Microservice B which validates a code using Spring Boot.

The operation of Microservice B throws an exception in case of the validation has been bad. Then I handled in the Microservices and return a HttpStatus.UNPROCESSABLE_ENTITY (422) like next:

@ExceptionHandler({
       ValidateException.class
    })
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    @ResponseBody
    public Object validationException(final HttpServletRequest request, final validateException exception) {
        log.error(exception.getMessage(), exception);
        error.setErrorMessage(exception.getMessage());
        error.setErrorCode(exception.getCode().toString());
        return error;
    }

So, when Microservice A calls to B in a interface as next:

@Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
@RequestLine("GET /other")
void otherOperation(@Param("other")  String other );

@Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
@RequestLine("GET /code/validate")
Boolean validate(@Param("prefix") String prefix);

static PromotionClient connect() {

    return Feign.builder()
        .encoder(new GsonEncoder())
        .decoder(new GsonDecoder())
        .target(PromotionClient.class, Urls.SERVICE_URL.toString());
}

and the validations fails it returns a internal error 500 with next message:

{
  "timestamp": "2016-08-05T09:17:49.939+0000",
  "status": 500,
  "error": "Internal Server Error",
  "exception": "feign.FeignException",
  "message": "status 422 reading Client#validate(String); content:\n{\r\n  \"errorCode\" : \"VALIDATION_EXISTS\",\r\n  \"errorMessage\" : \"Code already exists.\"\r\n}",
  "path": "/code/validate"
}

But I need to return the same as the Microservice operation B.

Which would be the best ways or techniques to propagate Status and Exceptions through microservices using Netflix Feign?

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Pau
  • 14,917
  • 14
  • 67
  • 94

6 Answers6

26

You could use a feign ErrorDecoder

https://github.com/OpenFeign/feign/wiki/Custom-error-handling

Here is an example

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 MyBadRequestException();
        }
        return defaultErrorDecoder.decode(methodKey, response);
    }

}

For spring to pick up the ErrorDecoder you have to put it on the ApplicationContext:

@Bean
public MyErrorDecoder myErrorDecoder() {
  return new MyErrorDecoder();
}
Mathias Dpunkt
  • 11,594
  • 4
  • 45
  • 70
  • 1
    How could I know if the error comes from otherOperation() or validate()? I'm sharing the Feign.builder() so I would have the same ErrorDecoder and my codes and message will not be the same :( – Pau Aug 05 '16 at 13:51
  • 1
    I think you can use the `methodKey` parameter of the `decode` method. According to the API doc it should contain `{@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()}` So that can give you a hint on the context. – Mathias Dpunkt Aug 05 '16 at 14:48
  • There is a special case flag for 404 errors, passing decode404 = true in the Feign.builder() will return 404 and nothing else. – Tito Apr 19 '17 at 16:28
4

Shameless plug for a little library I did that uses reflection to dynamically rethrow checked exceptions (and unchecked if they are on the Feign interface) based on an error code returned in the body of the response.

More information on the readme : https://github.com/coveo/feign-error-decoder

jebeaudet
  • 1,533
  • 16
  • 15
4

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 the way you need it (see here):

@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 from Microservice B. You can go further and also return the original response body:

response.getOutputStream().write(e.content());
Moritz
  • 1,954
  • 2
  • 18
  • 28
  • What should be the return type of handleFeignStatusException method? returning "feignError" is sending "feignError" as response body. Also there seems to be no body when exception is 401 status code. – Juan Rojas Jul 03 '20 at 05:11
  • @JuanRojas see the [ExceptionHandler documentation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html) for details of the return type. You can use `@ResponseBody` if you want the return type to be the response body. – Moritz Jul 03 '20 at 06:58
  • Feign exception status is always 500 and I can't see in debug watch the initial HTTP-status returned by other microservice. – Zon Jul 14 '21 at 08:36
1

Write your custom exception mapper and register it. You can customize responses.

Complete example is here

public class GenericExceptionMapper implements ExceptionMapper<Throwable> {

    @Override
    public Response toResponse(Throwable ex) {
        return Response.status(500).entity(YOUR_RETURN_OBJ_HERE).build();
    }

}
Tugrul
  • 1,760
  • 4
  • 24
  • 39
1

Since 2017 we've created a library that does this from annotations (making it fairly easy to, just like for requests/etc, to code this up by annotations).

it basically allows you to code error handling as follows:

@ErrorHandling(codeSpecific =
    {
        @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
        @ErrorCodes( codes = {403}, generate = ForbiddenException.class),
        @ErrorCodes( codes = {404}, generate = UnknownItemException.class),
    },
    defaultException = ClassLevelDefaultException.class
)
interface GitHub {

    @ErrorHandling(codeSpecific =
        {
            @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
            @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
        },
        defaultException = FailedToGetContributorsException.class
    )
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

You can find it in the OpenFeign organisation: https://github.com/OpenFeign/feign-annotation-error-decoder

disclaimer: I'm a contributor to feign and the main dev for that error decoder.

saintf
  • 11
  • 1
0

What we do is as follows:

Share common jar which contains exceptions with both microservices.

1.) In microservices A convert exception to a DTO class lets say ErrorInfo. Which will contain all the attributes of your custom exception with a String exceptionType, which will contain exception class name.

2.) When it is received at microservice B it will be handled by ErrorDecoder in microservice B and It will try to create an exception object from exceptionType as below:

@Override
public Exception decode(String methodKey, Response response) {       

ErrorInfo errorInfo = objectMapper.readValue(details, ErrorInfo.class);
Class exceptionClass;

Exception decodedException;

try {

    exceptionClass = Class.forName(errorInfo.getExceptionType());  

    decodedException = (Exception) exceptionClass.newInstance();

    return decodedException;

 }

 catch (ClassNotFoundException e) {

    return new PlatformExecutionException(details, errorInfo);

 }
  return defaultErrorDecoder.decode(methodKey, response);
 }
Asif Malek
  • 169
  • 1
  • 7