This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 0e9ce087dd fix(httpclient): handle missing upstream list during 
failover (#7483)
0e9ce087dd is described below

commit 0e9ce087dd19fb8dc981935340cab75e030f042e
Author: lymerin <[email protected]>
AuthorDate: Sun Oct 11 23:20:03 2026 +0800

    fix(httpclient): handle missing upstream list during failover (#7483)
---
 .../plugin/httpclient/DefaultRetryStrategy.java    |  6 +++-
 .../plugin/httpclient/RetryStrategyTest.java       | 21 +++++++++++
 .../plugin/httpclient/WebClientPluginTest.java     | 41 ++++++++++++++++++++++
 3 files changed, 67 insertions(+), 1 deletion(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/DefaultRetryStrategy.java
 
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/DefaultRetryStrategy.java
index 5ab621c1ad..799b928f7f 100644
--- 
a/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/DefaultRetryStrategy.java
+++ 
b/shenyu-plugin/shenyu-plugin-httpclient/src/main/java/org/apache/shenyu/plugin/httpclient/DefaultRetryStrategy.java
@@ -112,7 +112,11 @@ public class DefaultRetryStrategy<R> implements 
RetryStrategy<R> {
             final String selectorId = 
exchange.getAttribute(Constants.DIVIDE_SELECTOR_ID);
             final String loadBalance = 
exchange.getAttribute(Constants.LOAD_BALANCE);
             //always query the latest available list
-            final List<Upstream> upstreamList = 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId)
+            final List<Upstream> availableUpstreamList = 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId);
+            if (Objects.isNull(availableUpstreamList)) {
+                return Mono.error(new 
ShenyuException("CANNOT_FIND_HEALTHY_UPSTREAM_URL_AFTER_FAILOVER"));
+            }
+            final List<Upstream> upstreamList = availableUpstreamList
                     .stream().filter(data -> {
                         final URI upstreamUri = URI.create(data.buildDomain());
                         for (URI needToExclude : exclude) {
diff --git 
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/RetryStrategyTest.java
 
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/RetryStrategyTest.java
index efea42791f..6f18a1ac8a 100644
--- 
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/RetryStrategyTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/RetryStrategyTest.java
@@ -161,6 +161,27 @@ public class RetryStrategyTest {
         assertEquals(URI.create(standby.buildDomain() + "/test?mode=retry"), 
uriCaptor.getValue());
     }
 
+    @Test
+    void testFailoverWithMissingUpstreamList() {
+        AbstractHttpClientPlugin<String> httpClientPlugin = 
mock(AbstractHttpClientPlugin.class);
+        DefaultRetryStrategy<String> strategy = new 
DefaultRetryStrategy<>(httpClientPlugin);
+        ServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.get("/test").build());
+        exchange.getAttributes().put(Constants.RETRY_STRATEGY, 
RetryEnum.FAILOVER.getName());
+        exchange.getAttributes().put(Constants.HTTP_URI, 
URI.create("http://localhost:8080/test";));
+        exchange.getAttributes().put(Constants.DIVIDE_SELECTOR_ID, 
"retry-missing-upstream");
+
+        StepVerifier.create(strategy.execute(Mono.error(new 
ConnectTimeoutException("connection timed out")), exchange, 
Duration.ofSeconds(5), 3))
+                .expectErrorSatisfies(error -> {
+                    assertTrue(error instanceof ResponseStatusException);
+                    ResponseStatusException exception = 
(ResponseStatusException) error;
+                    assertEquals(HttpStatus.SERVICE_UNAVAILABLE, 
exception.getStatusCode());
+                    
assertEquals("CANNOT_FIND_HEALTHY_UPSTREAM_URL_AFTER_FAILOVER", 
exception.getReason());
+                })
+                .verify();
+
+        verifyNoInteractions(httpClientPlugin);
+    }
+
     @Test
     void testFailoverSkipsBlockedUpstream() {
         AbstractHttpClientPlugin<String> plugin = 
mock(AbstractHttpClientPlugin.class);
diff --git 
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTest.java
 
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTest.java
index 5a8f66d568..52eb1f535c 100644
--- 
a/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-httpclient/src/test/java/org/apache/shenyu/plugin/httpclient/WebClientPluginTest.java
@@ -23,6 +23,8 @@ import org.apache.shenyu.common.enums.PluginEnum;
 import org.apache.shenyu.common.enums.RpcTypeEnum;
 import org.apache.shenyu.common.enums.UniqueHeaderEnum;
 import org.apache.shenyu.common.enums.RetryEnum;
+import org.apache.shenyu.loadbalancer.cache.UpstreamCacheManager;
+import org.apache.shenyu.loadbalancer.entity.Upstream;
 import 
org.apache.shenyu.plugin.httpclient.exception.ShenyuUpstreamStatusException;
 import org.apache.shenyu.plugin.api.ShenyuPluginChain;
 import org.apache.shenyu.plugin.api.context.ShenyuContext;
@@ -55,6 +57,7 @@ import 
org.springframework.web.reactive.function.client.ClientRequest;
 import org.springframework.web.reactive.function.client.ClientResponse;
 import org.springframework.web.reactive.function.client.ExchangeFunction;
 import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.server.ResponseStatusException;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Flux;
@@ -69,7 +72,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.BDDMockito.given;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 /**
@@ -218,6 +225,40 @@ public final class WebClientPluginTest {
                 .verify();
     }
 
+    @Test
+    public void testServerErrorAfterUpstreamRemoval() {
+        final String selectorId = "webclient-removed-upstream";
+        final UpstreamCacheManager cacheManager = 
UpstreamCacheManager.getInstance();
+        final Upstream upstream = 
Upstream.builder().url("localhost:8080").healthCheckEnabled(false).build();
+        cacheManager.submit(selectorId, Collections.singletonList(upstream));
+        final ServerWebExchange exchange = generateServerWebExchange();
+        exchange.getAttributes().put(Constants.HTTP_URI, 
URI.create("http://localhost:8080/test";));
+        exchange.getAttributes().put(Constants.RETRY_STRATEGY, 
RetryEnum.FAILOVER.getName());
+        exchange.getAttributes().put(Constants.HTTP_RETRY, 1);
+        exchange.getAttributes().put(Constants.DIVIDE_SELECTOR_ID, selectorId);
+        final ShenyuPluginChain chain = mock(ShenyuPluginChain.class);
+        when(exchangeFunction.exchange(any())).thenAnswer(invocation -> 
Mono.defer(() -> {
+            cacheManager.removeByKey(selectorId);
+            return 
Mono.just(ClientResponse.create(HttpStatus.INTERNAL_SERVER_ERROR).build());
+        }));
+        final WebClientPlugin plugin = new 
WebClientPlugin(WebClient.builder().exchangeFunction(exchangeFunction).build(), 
Constants.BYTES_PER_MB);
+
+        try {
+            StepVerifier.create(plugin.execute(exchange, chain))
+                    .expectErrorSatisfies(error -> {
+                        assertTrue(error instanceof ResponseStatusException);
+                        ResponseStatusException exception = 
(ResponseStatusException) error;
+                        assertEquals(HttpStatus.SERVICE_UNAVAILABLE, 
exception.getStatusCode());
+                        
assertEquals("CANNOT_FIND_HEALTHY_UPSTREAM_URL_AFTER_FAILOVER", 
exception.getReason());
+                    })
+                    .verify();
+            verify(exchangeFunction, times(1)).exchange(any());
+            verifyNoInteractions(chain);
+        } finally {
+            cacheManager.removeByKey(selectorId);
+        }
+    }
+
     @ParameterizedTest
     @EnumSource(HeaderUniqueStrategyEnum.class)
     public void testOutboundHeaderDeduplication(final HeaderUniqueStrategyEnum 
strategy) {

Reply via email to