Propagating Exceptions With OpenFeign and Spring – 用OpenFeign和Spring传播异常

最后修改: 2022年 8月 17日

1. Overview


We expect HTTP API calls between microservices to encounter occasional errors.

我们预计微服务之间的HTTP API调用会偶尔遇到错误。

In Spring Boot with OpenFeign, the default error handler propagates downstream errors, such as Not Found, as Internal Server Error. This is seldom the best way to convey the error. However, both Spring and OpenFeign allow us to provide our own error handling.

在使用OpenFeign,默认错误处理器将下游错误,如Not Found,作为Internal Server Error传播。这很少是最好的方式来传达错误。然而,Spring和OpenFeign都允许我们提供我们自己的错误处理。

In this article, we’ll see how default exception propagation works. We’ll also learn how to supply our own errors.


2. Default Exception Propagation Strategy


The Feign client makes interactions between microservices straightforward and highly configurable, using annotations and configuration properties. However, API calls might fail due to any random technical reason, bad user requests, or coding errors.


Fortunately, Feign and Spring have a sensible default implementation for error handling.


2.1. Default Exception Propagation in Feign


Feign uses the ErrorDecoder.Default class for its error handling. With this, whenever Feign receives any non-2xx status code, it passes that to the ErrorDecoder’s decode method. The decode method either returns a RetryableException if the HTTP response had a Retry-After header or it returns a FeignException otherwise. When retrying, if the request fails after the default number of retries, then the FeignException will be returned.

Feign使用ErrorDecoder.Default类来处理其错误。有了它,每当Feign收到任何非2xx状态代码时,它就会将其传递给ErrorDecoder的解码方法。decode 方法要么返回一个RetryableException如果HTTP响应有一个重试-。后头,否则它将返回一个FeignException。当重试时,如果请求在默认的重试次数后失败,那么将返回FeignException

The decode method stores the HTTP method key and response in the FeignException.


2.2. Default Exception Propagation in Spring Rest Controller

2.2.Spring Rest控制器中的默认异常传播

Whenever the RestController receives any unhandled exception, it returns a 500 Internal Server Error response to the client.

每当RestController收到任何未处理的异常就会向客户端返回一个500 Internal Server Error响应。

Also, Spring provides a well-structured error response with details such as timestamp, HTTP status code, error, and the path:


    "timestamp": "2022-07-08T08:07:51.120+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/myapp1/product/Test123"

Let’s take a deep dive into this with an example.


3. Example Application


Let’s imagine we need to build a simple microservice that returns product information from another external service.


First, let’s model the Product class with a few properties:


public class Product {
    private String id;
    private String productName;
    private double price;

Then, let’s implement the ProductController with the Get Product endpoint:

然后,让我们用Get Product端点实现ProductController

@RequestMapping(value ="myapp1")
public class ProductController {

    private ProductClient productClient;

    public ProductController(ProductClient productClient) {
        this.productClient = productClient;

    public Product getProduct(@PathVariable String id) {
        return productClient.getProduct(id);

Next, let’s see how to register the Feign Logger as a Bean:

接下来,让我们看看如何注册Feign Logger作为一个Bean

public class FeignConfig {

    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;

Finally, let’s implement the ProductClient to interface with the external API:


@FeignClient(name = "product-client", url="http://localhost:8081/product/", configuration = FeignConfig.class)
public interface ProductClient {
    @RequestMapping(value = "{id}", method = RequestMethod.GET")
    Product getProduct(@PathVariable(value = "id") String id);

Let’s now explore default error propagation using the above example.


4. Default Exception Propagation


4.1. Using WireMock Server


To experiment, we’ll need to use a mocking framework to simulate the service we’re calling.


First, let’s include the WireMockServer Maven dependency:

首先,让我们加入WireMockServer Maven依赖。


Then, let’s configure and start the WireMockServer:


WireMockServer wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);

The WireMockServer is started at the same host and port that the Feign client is configured to use.


4.2. Default Exception Propagation in Feign Client


Feign’s default error handler, ErrorDecoder.Default, always throws a FeignException.


Let’s mock the getProduct method with the WireMock.stubFor to make it appear to be unavailable:


String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))

assertThrows(FeignException.class, () -> productClient.getProduct(productId));

In the above test case, the ProductClient throws the FeignException when it encounters the 503 error from the downstream service.


Next, let’s try the same experiment but with a 404 Not Found response:


String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))

assertThrows(FeignException.class, () -> productClient.getProduct(productId));

Again, we’re getting a general FeignException. In this situation, perhaps the user requested something that was wrong and our Spring application needs to know that it’s a bad user request so that it can handle things differently.


We should note that FeignException does have a status property containing the HTTP status code, but a try/catch strategy routes exceptions based on their type, rather than their properties.


4.3. Default Exception Propagation in Spring Rest Controller

4.3.Spring Rest控制器中的默认异常传播

Let’s now see how the FeignException propagates back to the requester.


When the ProductController gets the FeignException from the ProductClientit passes that to its default error handling implementation provided by the framework.

ProductController获得FeignException ProductClient它将其传递给框架所提供的默认错误处理实现。

Let’s assert when the product service is unavailable:


String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))

mockMvc.perform(get("/myapp1/product/" + productId))

Here, we can see that we get the Spring INTERNAL_SERVER_ERROR. This default behavior is not always the best, as different service errors may require different outcomes.


5. Propagating Custom Exceptions in Feign With the ErrorDecoder


Instead of always returning the default FeignException, we should return some application-specific exceptions based on the HTTP status code.


Let’s override the decode method in a custom ErrorDecoder implementation:


public class CustomErrorDecoder implements ErrorDecoder {

    public Exception decode(String methodKey, Response response) {
        switch (response.status()){
            case 400:
                return new BadRequestException();
            case 404:
                return new ProductNotFoundException("Product not found");
            case 503:
                return new ProductServiceNotAvailableException("Product Api is unavailable");
                return new Exception("Exception while getting product details");

In our custom decode method, we’re returning different exceptions with a few application-specific ones to provide more context for the actual problem. We can also include more details in the application-specific exception messages.


We should note that the decode method returns the FeignException rather than throwing it.


Now, let’s configure the CustomErrorDecoder in the FeignConfig as a Spring Bean:

现在。让我们在CustomErrorDecoder中配置FeignConfig作为Spring 。spaces=”true”>FeignConfig中作为一个Spring Bean

public ErrorDecoder errorDecoder() {
   return new CustomErrorDecoder();

Alternatively, the CustomErrorDecoder can be configured directly in the ProductClient:


@FeignClient(name = "product-client-2", url = "http://localhost:8081/product/", 
   configuration = { FeignConfig.class, CustomErrorDecoder.class })

Then, let’s check whether the CustomErrorDecoder returns ProductServiceNotAvailableException:


String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))

  () -> productClient.getProduct(productId));

Again, let’s write a test case to assert the ProductNotFoundException when the product is not present:


String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))

  () -> productClient.getProduct(productId));

While we’re now providing a variety of exceptions from the Feign client, Spring will still produce a generic internal server error when it catches them all. Since this is not what we desire, let’s see how we can improve on that.


6. Propagating Custom Exceptions in Spring Rest Controller

6.在Spring Rest Controller中传播自定义异常

As we’ve seen, the default Spring Boot error handler provides a generic error response. API Consumers might need detailed information with relevant error responses. Ideally, the error response should be able to explain the problem and help in debugging.

正如我们所见,默认的Spring Boot错误处理程序提供了一个通用的错误响应。API消费者可能需要相关错误响应的详细信息。理想情况下,错误响应应该能够解释问题并帮助调试。

We could override the default exception handler in the RestController in many ways.


We’ll look into one such approach to handle errors with the RestControllerAdvice annotation.


6.1. Using @RestControllerAdvice


The @RestControllerAdvice annotation allows us to consolidate multiple exceptions into a single, global error handling component. 


Let’s imagine a scenario where the ProductController needs to return a different custom error response based on the downstream error.

让我们设想这样一个场景:ProductController 需要根据下游的错误返回一个不同的自定义错误响应。

First, let’s create the ErrorResponse class to customize the error response:


public class ErrorResponse {

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    private Date timestamp;

    @JsonProperty(value = "code")
    private int code;

    @JsonProperty(value = "status")
    private String status;
    @JsonProperty(value = "message")
    private String message;
    @JsonProperty(value = "details")
    private String details;

Now, let’s subclass the ResponseEntityExceptionHandler and include the @ExceptionHandler annotation with the error handlers:

现在,让我们对 ResponseEntityExceptionHandler进行子类化,并将@ExceptionHandler注解与错误处理程序一起加入。

public class ProductExceptionHandler extends ResponseEntityExceptionHandler {

    public ResponseEntity<ErrorResponse> handleProductServiceNotAvailableException(ProductServiceNotAvailableException exception, WebRequest request) {
        return new ResponseEntity<>(new ErrorResponse(

    public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException exception, WebRequest request) {
        return new ResponseEntity<>(new ErrorResponse(

In the above code, the ProductServiceNotAvailableException returns as an INTERNAL_SERVER_ERROR response to the client. In contrast, a user-specific error like ProductNotFoundException is handled differently and returns as a NOT_FOUND response.


6.2. Testing the Spring Rest Controller

6.2.测试Spring Rest控制器

Let’s test the ProductController when product service is unavailable:


String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))

MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))

ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(500, errorResponse.getCode());
assertEquals("Product Api is unavailable", errorResponse.getMessage());

Again, let’s test the same ProductController but with a product not found error:


String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))

MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))

ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(404, errorResponse.getCode());
assertEquals("Product not found", errorResponse.getMessage());

The above tests show how the ProductController returns different error responses based on the downstream error.


If we hadn’t implemented our CustomErrorDecoder, then the RestControllerAdvice is required to handle the default FeignException as a fallback to have a generic error response.


7. Conclusion


In this article, we’ve explored how the default error handling is implemented in Feign and Spring.


Also, we’ve seen how we can customize that in Feign client with CustomErrorDecoder and in the Rest Controller with RestControllerAdvice.

此外,我们已经看到我们如何在Feign客户端中用CustomErrorDecoder在Rest Controller中用RestControllerAdvice进行定制。

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