Error Handling with ErrorDecoder

What Happens on 4xx and 5xx

By default, any non-2xx response makes Feign throw FeignException (subclasses like FeignException.NotFound, FeignException.ServiceUnavailable). Letting that bubble up gives the client a confusing 500. Translate remote errors into your own exceptions with an ErrorDecoder:

public class ProductErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new ErrorDecoder.Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        return switch (response.status()) {
            case 404 -> new ProductNotFoundException("Product not found (" + methodKey + ")");
            case 409 -> new OutOfStockException("Not enough stock");
            case 503 -> new ServiceUnavailableException("product-service is unavailable");
            default  -> defaultDecoder.decode(methodKey, response);
        };
    }
}

// register it only for this client (class NOT annotated with @Configuration,
// otherwise it would apply to every Feign client)
class ProductClientConfig {
    @Bean
    ErrorDecoder productErrorDecoder() {
        return new ProductErrorDecoder();
    }
}

@FeignClient(name = "product-service", path = "/api/products", configuration = ProductClientConfig.class)
public interface ProductClient { /* ... */ }

Map to a Clean API Response

@RestControllerAdvice
class OrderExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    ProblemDetail notFound(ProductNotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
                "One of the products in your order no longer exists");
    }

    @ExceptionHandler(ServiceUnavailableException.class)
    ProblemDetail unavailable(ServiceUnavailableException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.SERVICE_UNAVAILABLE,
                "We could not place your order right now. Please retry in a minute.");
    }
}

Example

curl -i -X POST localhost:8080/api/orders -H "Content-Type: application/json" \
  -d '{"userId": 42, "productId": 999, "quantity": 1}'

Example output

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{"type":"about:blank","title":"Unprocessable Entity","status":422,
 "detail":"One of the products in your order no longer exists","instance":"/api/orders"}