api-gateway auth request interceptor. That will retry or return default response based on response and response status also modifies response and sends back to client
@Configuration
class GatewayConfig(
private val errorFilter: GlobalErrorFilter,
){
@Bean
fun authRequestFilter(
builder: RouteLocatorBuilder,
) = builder.routes()
.route(authService.name) { r ->
r.path(authService.path)
.filters { f ->
f.retry { retryConfig ->
retryConfig.apply {
setStatuses(
HttpStatus.PRECONDITION_FAILED, // validation token verification failure
HttpStatus.BAD_REQUEST
)
retries = RETRY_ATTEMPTS.toInt()
setBackoff(
INITIAL_RETRY_DELAY,
MAX_DELAY_BETWEEN_RETRY,
RETRY_FACTOR,
false
)
setMethods( // for now retry only on these methods
HttpMethod.GET,
HttpMethod.POST
)
setExceptions(
ConnectException::class.java,
)
}
}.circuitBreaker { config ->
config.setName("fallback") // this name is taken from the application.properties file. MUST NOT BE CHANGED
.setStatusCodes( // set of status code that will cause circuit breaker to trigger
setOf(
HttpStatus.INTERNAL_SERVER_ERROR.value().toString()
)
)
.setFallbackUri("forward:${Endpoint.FALLBACK}") // fallback route for circuit breaker
}.filter(
errorFilter,
FILTER_ORDER // -2
).modifyResponseBody( // modify response from user-service
ResponseWrapper::class.java,
Any::class.java
) { exchange, response ->
if (response.status == ResponseStatus.SUCCESS && response.payload != null)
Mono.just(response.payload)
else Mono.just(
ResponseWrapper(
status = response.status,
payload = response.payload ?: response.status.value,
)
)
}
}.uri(authService.uri)
}
.build()!!
}
@Component
@Primary
class GlobalErrorFilter(
private val mapper: ObjectMapper,
) : GatewayFilter {
override fun filter(
exchange: ServerWebExchange,
chain: GatewayFilterChain,
) = chain.filter(exchange)
.onErrorResume { handleAuthenticationError(exchange, it) }
fun handleAuthenticationError(
exchange: ServerWebExchange,
error: Throwable,
)= when (error) {
is NonRetryableAuthenticationException -> error.status to ResponseWrapper(
status = error.responseStatus,
payload = error.message ?: error.responseStatus.value
)
is RetryableAuthenticationException -> error.status to ResponseWrapper(
status = error.responseStatus,
payload = error.message ?: error.responseStatus.value
)
else -> {
logger.error("Unexpected error: ${error.message}")
HttpStatus.INTERNAL_SERVER_ERROR to ResponseWrapper(
status = ResponseStatus.INTERNAL_SERVER_ERROR,
payload = ResponseStatus.INTERNAL_SERVER_ERROR.value
)
}
}.let { (statusCode, responseWrapper) ->
writeErrorResponse(exchange, statusCode, responseWrapper)
}
private fun writeErrorResponse(
exchange: ServerWebExchange,
statusCode: HttpStatusCode,
responseWrapper: ResponseWrapper<*>,
): Mono<Void> {
exchange.response.statusCode = statusCode
exchange.response.headers.contentType = MediaType.APPLICATION_JSON
return exchange.response.writeWith(
Mono.fromCallable { mapper.writeValueAsBytes(responseWrapper) }
.map { exchange.response.bufferFactory().wrap(it) }
)
}
companion object {
private val logger = LoggerFactory.getLogger(JWTAuthenticationFilter::class.java)
}
}
resilience4j.circuitbreaker.instances.fallback.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.fallback.sliding-window-size=10
resilience4j.circuitbreaker.instances.fallback.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.fallback.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.fallback.wait-duration-in-open-state.seconds=30
resilience4j.circuitbreaker.instances.fallback.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.fallback.slow-call-duration-threshold.seconds=3
resilience4j.circuitbreaker.instances.fallback.slow-call-rate-threshold=50
resilience4j.circuitbreaker.instances.fallback.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.fallback.sliding-window-type=count_based
request is intercepted by circuitBreaker before the reponse is comming from authserivce
Removing retry and circuitBreaker block fixes the issue.
But I need to those. :)
Don't know What am i doing wrong.
Appriciate any and all help. Thank You. :)
Your fundamental problem's using a GatewayFilter with onErrorResume for global error handling, you intercept any Throwable in the reactive chain and terminate the exchange by writing a response immediately. The proper approach should be to use a dedicated implementation of ErrorWebExceptionHandler, which acts as a last-resort handler for the entire application. Let the Retry and CircuitBreaker filters handle the errors they're configured for and use a global exception handler for everything else.
1. Remove the custom GlobalErrorFilter from your route definition in GatewayConfig. It's catching exceptions prematurely, before Retry and CircuitBreaker filters can inspect the response status code. This prevents them from ever triggering.
// .filter(
// errorFilter,
// FILTER_ORDER
// )
.modifyResponseBody(
2. Create a proper global error handler. It'll catch any unhandled exceptions from your gateway application, including those that Retry and CircuitBreaker filters don't handle.
@Component
@Order(-1)
class GlobalErrorWebExceptionHandler(
private val mapper: ObjectMapper
) : ErrorWebExceptionHandler {
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
val (statusCode, responseWrapper) = when (ex) {
is io.github.resilience4j.circuitbreaker.CallNotPermittedException ->
HttpStatus.SERVICE_UNAVAILABLE to ResponseWrapper(
status = ResponseStatus.SERVICE_UNAVAILABLE,
payload = "Service is temporarily unavailable."
)
else ->
HttpStatus.INTERNAL_SERVER_ERROR to ResponseWrapper(
status = ResponseStatus.INTERNAL_SERVER_ERROR,
payload = "An unexpected internal error occurred."
)
}
}
}
3. Update your gateway configuration with your new error handler. And also adjust your Retry configuration to follow best practices. Retrying on 4xx status codes is generally an anti-pattern, as these's client errors and are unlikely to succeed on retry without changes to the request. You should primarily retry on transient server errors (5xx) or connection issues.
.filters { f ->
f.retry { retryConfig ->
retryConfig.setStatuses(
HttpStatus.INTERNAL_SERVER_ERROR,
HttpStatus.SERVICE_UNAVAILABLE
)
retryConfig.setExceptions(
java.io.IOException::class.java
)
}.circuitBreaker { config ->
config.setName("fallback")
.setFallbackUri("forward:/fallback/auth")
}