From 5df2544ff8b102079cbcf07aa2e1d91da6476b21 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Thu, 23 Apr 2026 09:23:04 +0000 Subject: [PATCH 1/2] fix: Catch the issues with service unavailable other than the Connect Signed-Off-By: Jakub Balhar --- .../config/NettyRoutingFilterApiml.java | 16 ++++++++- .../config/NettyRoutingFilterApimlTest.java | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java index e2193407eb..e375eca962 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java @@ -11,6 +11,7 @@ package org.zowe.apiml.gateway.config; import io.netty.channel.ChannelOption; +import io.netty.channel.ConnectTimeoutException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; @@ -25,6 +26,7 @@ import reactor.netty.http.client.HttpClient; import java.net.ConnectException; +import java.net.NoRouteToHostException; import java.time.Duration; import java.util.List; import java.util.Optional; @@ -84,14 +86,26 @@ protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { return super.filter(exchange, chain).onErrorResume(e -> { - if (e.getCause() instanceof ConnectException) { + if (isServiceUnavailable(e)) { log.debug("Connection to {} was not established: {}", exchange.getRequest().getURI(), e.getMessage()); var uri = exchange.getRequest().getURI(); return Mono.error(new ServiceNotAccessibleException(String.format("Service is not available at %s://%s:%d", uri.getScheme(), uri.getHost(), uri.getPort()), e)); } return Mono.error(e); }); + } + static boolean isServiceUnavailable(Throwable e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof ConnectException + || cause instanceof ConnectTimeoutException + || cause instanceof NoRouteToHostException) { + return true; + } + cause = cause.getCause(); + } + return false; } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java index d2c8927346..797213f41e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApimlTest.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.netty.channel.ChannelOption; +import io.netty.channel.ConnectTimeoutException; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import org.hamcrest.Matchers; @@ -28,6 +29,8 @@ import reactor.netty.http.client.HttpClient; import javax.net.ssl.SSLException; +import java.net.ConnectException; +import java.nio.channels.ClosedChannelException; import java.time.Duration; import static io.restassured.RestAssured.given; @@ -158,6 +161,39 @@ void givenTimeoutAndRequirementsForClientCert_whenGetHttpClient_thenCallWithoutC } + @Nested + class IsServiceUnavailable { + + @Test + void givenConnectTimeoutException_whenIsServiceUnavailable_thenReturnsTrue() { + assertTrue(NettyRoutingFilterApiml.isServiceUnavailable(new ConnectTimeoutException("connect timed out"))); + } + + @Test + void givenConnectException_whenIsServiceUnavailable_thenReturnsTrue() { + assertTrue(NettyRoutingFilterApiml.isServiceUnavailable(new ConnectException("connection refused"))); + } + + @Test + void givenConnectExceptionNestedInClosedChannelException_whenIsServiceUnavailable_thenReturnsTrue() { + ClosedChannelException outer = new ClosedChannelException(); + ConnectException inner = new ConnectException("connection refused"); + outer.initCause(inner); + assertTrue(NettyRoutingFilterApiml.isServiceUnavailable(outer)); + } + + @Test + void givenUnrelatedException_whenIsServiceUnavailable_thenReturnsFalse() { + assertFalse(NettyRoutingFilterApiml.isServiceUnavailable(new RuntimeException("some other error"))); + } + + @Test + void givenSslException_whenIsServiceUnavailable_thenReturnsFalse() { + assertFalse(NettyRoutingFilterApiml.isServiceUnavailable(new javax.net.ssl.SSLException("cert error"))); + } + + } + @Nested class UnavailableService extends AcceptanceTestWithMockServices { From 6e2aa631a69ff7ad355778f358cb3383a37f6894 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Thu, 30 Apr 2026 13:53:09 +0200 Subject: [PATCH 2/2] Clear isServiceUnavailable Signed-off-by: Jakub Balhar --- .../zowe/apiml/gateway/config/NettyRoutingFilterApiml.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java index e375eca962..54283927ce 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/NettyRoutingFilterApiml.java @@ -95,15 +95,14 @@ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { }); } - static boolean isServiceUnavailable(Throwable e) { - Throwable cause = e; - while (cause != null) { + static boolean isServiceUnavailable(Throwable error) { + if (error != null) { + Throwable cause = e.getCause(); if (cause instanceof ConnectException || cause instanceof ConnectTimeoutException || cause instanceof NoRouteToHostException) { return true; } - cause = cause.getCause(); } return false; }