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.52k stars 3.32k forks source link

Using resilience4J timelimiter, gateway timeouts return are not caught by fallback of the CircuitBreaker filter #2528

Closed Eredrim closed 2 years ago

Eredrim commented 2 years ago

Describe the bug Versions : Spring cloud 2021.0.0 Spring boot 2.6.3

Description: I created a gateway project with resilience4j as CircuitBreaker to catch exceptions and return custom HTTP errors. I configured the timelimiter to provide a timeout after 60s. Everything works great with GET requests but with other HTTP methods (like POST or PUT), the CircuitBreaker fallback doesn't catch the exception and a 405 error is returned. I use this circuitBreaker in combination of a discoveryLocator (sample below).

Sample Config sample:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
          predicates:
            - name: Path
              args:
                pattern: "'/'+serviceId+'/**'"
          filters:
            - name: CircuitBreaker
              args:
                name: "'gatewayCircuitBreaker'"
                fallbackUri: "'forward:/fallback'"
            - name: RewritePath
              args:
                regexp: "'/' + serviceId + '/(?<remaining>.*)'"
                replacement: "'/${remaining}'"

CircuitBreakerFactory Bean:

@Configuration
public class GatewayCircuitBreakerConfig {

    /**
     * Allows loading of circuitBreaker configuration from Spring yaml config
     */
    @Bean
    public ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory(CircuitBreakerRegistry circuitBreakerRegistry, TimeLimiterRegistry timeLimiterRegistry) {
        return new ReactiveResilience4JCircuitBreakerFactory(circuitBreakerRegistry, timeLimiterRegistry);
    }
}

Fallback controller:

@Slf4j
@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping
    public ResponseEntity<String> getFallback(ServerWebExchange exchange) {
        Throwable cause = exchange.getAttribute(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
        Throwable rootCause = ExceptionUtils.getRootCause(cause);
        log.debug("Failure, cause was : ", cause);

        if(rootCause == null && cause instanceof java.util.concurrent.TimeoutException) {
            // Gateway Timeout
            return buildGatewayTimeoutResponse();
        } else {
            // Other error
            return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, cause);
        }
    }
}
ryanjbaxter commented 2 years ago

Can you provide a complete, minimal, verifiable sample that reproduces the problem? It should be available as a GitHub (or similar) project or attached to this issue as a zip file.

spring-cloud-issues commented 2 years ago

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-cloud-issues commented 2 years ago

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Eredrim commented 2 years ago

Hello,

After spending time to produce a minimal sample, I figured out my issue was not one. In my feedback controller, the getFallback() method should have been written with a @RequestMapping instead of @GetMapping to catch all incoming requests.

So, here is the corrected version of my fallback controller:

@Slf4j
@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @RequestMapping
    public ResponseEntity<String> getFallback(ServerWebExchange exchange) {
        Throwable cause = exchange.getAttribute(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
        Throwable rootCause = ExceptionUtils.getRootCause(cause);
        log.debug("Failure, cause was : ", cause);

        if(rootCause == null && cause instanceof java.util.concurrent.TimeoutException) {
            // Gateway Timeout
            return buildGatewayTimeoutResponse();
        } else {
            // Other error
            return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, cause);
        }
    }
}