spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.49k stars 3.31k forks source link

Adding circuitBreaker filter in mvc throws IllegalArgumentException #3327

Open bright-k opened 5 months ago

bright-k commented 5 months ago

i using java 21 spring boot 3.2.4 spring cloud dependencies 2023.0.1

Adding circuitBreaker filter in mvc throws IllegalArgumentException

An error occurs when the circuitBreaker filter is set as below and passes through the registered route.

spring-cloud-starter-circuitbreaker-reactor-resilience4j dependency is added

spring:
    gateway.mvc:
      routes:
        - id: test-routes
          predicates:
            - name: Path
              args:
                patterns: |
                  /api-test/**,
                  /api-test2/**
          filters:
            - StripPrefix=1
            - CircuitBreaker=myCircuitBreaker
          uri: http://localhost:8081
Caused by: java.lang.IllegalArgumentException: A CircuitBreaker must have an id.
    at org.springframework.util.Assert.hasText(Assert.java:240)
    at org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory.create(Resilience4JCircuitBreakerFactory.java:125)
    at org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.lambda$circuitBreaker$7(CircuitBreakerFilterFunctions.java:80)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$0(HandlerFilterFunction.java:58)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$ofRequestProcessor$3(HandlerFilterFunction.java:83)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$0(HandlerFilterFunction.java:58)
    at org.springframework.cloud.gateway.server.mvc.config.RouterFunctionHolderFactory.lambda$getRouterFunction$3(RouterFunctionHolderFactory.java:154)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$0(HandlerFilterFunction.java:58)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$ofRequestProcessor$3(HandlerFilterFunction.java:83)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$1(HandlerFilterFunction.java:59)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$1(HandlerFilterFunction.java:59)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$1(HandlerFilterFunction.java:59)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$andThen$1(HandlerFilterFunction.java:59)
    at org.springframework.web.servlet.function.HandlerFilterFunction.lambda$apply$2(HandlerFilterFunction.java:70)
    at org.springframework.web.servlet.function.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:108)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)

Did I miss anything?

karthik20 commented 5 months ago

I have the same issue. The yaml based configuration doesn't seem to register the circuitbreaker CircuitBreakerFilterFunctions. But doing via WebMvc.fn helps.

Reference: https://github.com/karthik20/spring-cloud-gateway-cb/blob/main/gateway/src/main/java/com/karthik/gateway/config/router/RouterConfig.java

@Bean
public RouterFunction<ServerResponse> routeConfig() {
    return route("customer_route")
            .route(path("/customer/**").or(path("/customer")), http("http://localhost:8080"))
            .filter(circuitBreaker(config -> config.setId("customerCircuitBreaker")
                    .setStatusCodes("500")))
            .build();
    }
jivebreaddev commented 4 months ago

May I handle this issue? I have an idea to work on this.

jivebreaddev commented 3 months ago

Given Situation: yaml file fails to bind to the CircuitBreaker HandlerFilterFunction

// given properties.yaml
spring:
    gateway.mvc:
      routes:
        - id: test-routes
          predicates:
            - name: Path
              args:
                patterns: |
                  /api-test/**,
                  /api-test2/**
          filters:
            - StripPrefix=1
            - CircuitBreaker=myCircuitBreaker
          uri: http://localhost:8081

// Thrown Exception      
Caused by: java.lang.IllegalArgumentException: A CircuitBreaker must have an id.
    at org.springframework.util.Assert.hasText(Assert.java:240)
    at org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory.create(Resilience4JCircuitBreakerFactory.java:125)
    at org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.lambda$cir

// Code

public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(CircuitBreakerConfig config) {
    Set<HttpStatusCode> failureStatuses = config.getStatusCodes().stream()
            .map(status -> HttpStatusHolder.valueOf(status).resolve()).collect(Collectors.toSet());
    return (request, next) -> {
        CircuitBreakerFactory<?, ?> circuitBreakerFactory = MvcUtils.getApplicationContext(request)
                .getBean(CircuitBreakerFactory.class);
        // TODO: cache
        CircuitBreaker circuitBreaker = **circuitBreakerFactory.create(config.getId());**

image

  1. findOperation method will match HandlerFilterFuction(given Parameters) with proper Arguments
    @Shortcut
    public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(String id) {
        return circuitBreaker(config -> config.setId(id));
    }

    public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(String id, URI fallbackUri) {
        return circuitBreaker(config -> config.setId(id).setFallbackUri(fallbackUri));
    }

    public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(String id, String fallbackPath) {
        return circuitBreaker(config -> config.setId(id).setFallbackPath(fallbackPath));
    }

    public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(
            Consumer<CircuitBreakerConfig> configConsumer) {
        CircuitBreakerConfig config = new CircuitBreakerConfig();
        configConsumer.accept(config);
        return circuitBreaker(config);
    }

        **@Shortcut
    @Configurable
    public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(CircuitBreakerConfig config) {**
        Set<HttpStatusCode> failureStatuses = config.getStatusCodes().stream()
                .map(status -> HttpStatusHolder.valueOf(status).resolve()).collect(Collectors.toSet());
        return (request, next) -> {
            CircuitBreakerFactory<?, ?> circuitBreakerFactory = MvcUtils.getApplicationContext(request)
                    .getBean(CircuitBreakerFactory.class);
            // TODO: cache
            CircuitBreaker circuitBreaker = circuitBreakerFactory.create(config.getId());
            return circuitBreaker.run(() -> {
                try {
                    ServerResponse serverResponse = next.handle(request);
                    // on configured status code, throw exception
                    if (failureStatuses.contains(serverResponse.statusCode())) {
                        throw new CircuitBreakerStatusCodeException(serverResponse.statusCode());
                    }
  1. Debugging shows that method with @Configurable(which was added here #3172) has been matched but it should have been bound to circuitBreaker(String id)

image

  1. This is result of this line of code where @Configurable will always return by bypassing existing logic. image

  2. My code changes:

image

  1. given HandlerFunctions, @Configurable will be processed the last as it will be matched. Also, Unordered Map tolist will introduce uncertain behaviors.
  2. Better Logging statements to observe which properties were failed to bind.
Snorky35 commented 1 month ago

Hi all,

I was facing the same issue and tried with the latest version (2023.0.3), but it doesn't seem to be fixed Do you confirm ? Hope it will be part of the next update, thanks