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 c5099d1175 fix(httpclient): stop failover retrying the same failed 
upstream when it has no explicit port (#7489)
c5099d1175 is described below

commit c5099d117515996fa9d9d8294ebb58729a5bc469
Author: lymerin <[email protected]>
AuthorDate: Thu Oct 8 14:17:52 2026 +0800

    fix(httpclient): stop failover retrying the same failed upstream when it 
has no explicit port (#7489)
---
 .../plugin/httpclient/DefaultRetryStrategy.java    | 14 +++-
 .../plugin/httpclient/RetryStrategyTest.java       | 89 ++++++++++++++++++++++
 2 files changed, 101 insertions(+), 2 deletions(-)

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 a52e68fedb..79b6727dfd 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
@@ -20,6 +20,7 @@ package org.apache.shenyu.plugin.httpclient;
 import org.apache.shenyu.common.constant.Constants;
 import org.apache.shenyu.common.enums.RetryEnum;
 import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.common.utils.UriUtils;
 import org.apache.shenyu.loadbalancer.cache.UpstreamCacheManager;
 import org.apache.shenyu.loadbalancer.entity.Upstream;
 import org.apache.shenyu.plugin.api.utils.RequestUrlUtils;
@@ -39,6 +40,7 @@ import java.time.Duration;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -111,9 +113,9 @@ public class DefaultRetryStrategy<R> implements 
RetryStrategy<R> {
             //always query the latest available list
             final List<Upstream> upstreamList = 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId)
                     .stream().filter(data -> {
-                        final String trimUri = data.getUrl().trim();
+                        final URI upstreamUri = URI.create(data.buildDomain());
                         for (URI needToExclude : exclude) {
-                            if ((needToExclude.getHost() + ":" + 
needToExclude.getPort()).equals(trimUri)) {
+                            if (isSameUpstream(needToExclude, upstreamUri)) {
                                 return false;
                             }
                         }
@@ -136,4 +138,12 @@ public class DefaultRetryStrategy<R> implements 
RetryStrategy<R> {
                     .doOnError(e -> LOG.error(e.getMessage(), e));
         });
     }
+
+    private boolean isSameUpstream(final URI first, final URI second) {
+        final String firstScheme = first.getScheme().toLowerCase(Locale.ROOT);
+        final String secondScheme = 
second.getScheme().toLowerCase(Locale.ROOT);
+        return firstScheme.equals(secondScheme)
+                && first.getHost().equalsIgnoreCase(second.getHost())
+                && UriUtils.getActualPort(firstScheme, first.getPort()) == 
UriUtils.getActualPort(secondScheme, second.getPort());
+    }
 }
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 e59214edda..7b8e7e71ba 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
@@ -18,19 +18,40 @@
 package org.apache.shenyu.plugin.httpclient;
 
 import io.netty.channel.ConnectTimeoutException;
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.enums.RetryEnum;
+import org.apache.shenyu.loadbalancer.cache.UpstreamCacheManager;
+import org.apache.shenyu.loadbalancer.entity.Upstream;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.springframework.http.HttpStatus;
 import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ResponseStatusException;
 import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
+import java.net.URI;
 import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
 
 /**
  * retry strategy test.
@@ -85,6 +106,58 @@ public class RetryStrategyTest {
                 .verify();
     }
 
+    @ParameterizedTest
+    @CsvSource({
+        "http://localhost/test, http://, localhost",
+        "http://localhost/test, http://, localhost:80",
+        "https://localhost/test, https://, localhost:443",
+        "http://localhost:8080/other?ignored=true, http://, localhost:8080"
+    })
+    void testFailoverExcludesSameUpstream(final String currentUri, final 
String protocol, final String url) {
+        AbstractHttpClientPlugin<String> plugin = 
mock(AbstractHttpClientPlugin.class);
+        ServerWebExchange exchange = createFailoverExchange(currentUri);
+        Upstream upstream = 
Upstream.builder().protocol(protocol).url(url).build();
+        UpstreamCacheManager cacheManager = mock(UpstreamCacheManager.class);
+        
when(cacheManager.findUpstreamListBySelectorId("selector-7475")).thenReturn(Collections.singletonList(upstream));
+
+        try (MockedStatic<UpstreamCacheManager> cacheMock = 
mockStatic(UpstreamCacheManager.class)) {
+            
cacheMock.when(UpstreamCacheManager::getInstance).thenReturn(cacheManager);
+            StepVerifier.create(new 
DefaultRetryStrategy<>(plugin).execute(Mono.error(new 
TimeoutException("upstream failed")),
+                    exchange, Duration.ofSeconds(5), 2))
+                    .expectErrorSatisfies(this::assertFailoverExhausted)
+                    .verify();
+        }
+        verifyNoInteractions(plugin);
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        "http://, standby:8080",
+        "http://, localhost:8081",
+        "https://, localhost:8080"
+    })
+    void testFailoverSelectsDifferentUpstream(final String protocol, final 
String url) {
+        AbstractHttpClientPlugin<String> plugin = 
mock(AbstractHttpClientPlugin.class);
+        ServerWebExchange exchange = 
createFailoverExchange("http://localhost:8080/test?mode=retry";);
+        Upstream failed = Upstream.builder().url("localhost:8080").build();
+        Upstream standby = 
Upstream.builder().protocol(protocol).url(url).build();
+        UpstreamCacheManager cacheManager = mock(UpstreamCacheManager.class);
+        
when(cacheManager.findUpstreamListBySelectorId("selector-7475")).thenReturn(Arrays.asList(failed,
 standby));
+        when(plugin.getCachedRequestBody(exchange)).thenReturn(Flux.empty());
+        when(plugin.doRequest(eq(exchange), eq("GET"), any(URI.class), 
any())).thenReturn(Mono.just("success"));
+
+        try (MockedStatic<UpstreamCacheManager> cacheMock = 
mockStatic(UpstreamCacheManager.class)) {
+            
cacheMock.when(UpstreamCacheManager::getInstance).thenReturn(cacheManager);
+            StepVerifier.create(new 
DefaultRetryStrategy<>(plugin).execute(Mono.error(new 
TimeoutException("upstream failed")),
+                    exchange, Duration.ofSeconds(5), 2))
+                    .expectNext("success")
+                    .verifyComplete();
+        }
+        ArgumentCaptor<URI> uriCaptor = ArgumentCaptor.forClass(URI.class);
+        verify(plugin).doRequest(eq(exchange), eq("GET"), uriCaptor.capture(), 
any());
+        assertEquals(URI.create(standby.buildDomain() + "/test?mode=retry"), 
uriCaptor.getValue());
+    }
+
     @Test
     void testFixedRetryStrategyRetriesTransientGetFailures() {
         AbstractHttpClientPlugin<String> httpClientPlugin = 
mock(AbstractHttpClientPlugin.class);
@@ -139,4 +212,20 @@ public class RetryStrategyTest {
 
         assertEquals(1, attempts.get());
     }
+
+    private ServerWebExchange createFailoverExchange(final String currentUri) {
+        ServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.get("/test?mode=retry"));
+        exchange.getAttributes().put(Constants.HTTP_URI, 
URI.create(currentUri));
+        exchange.getAttributes().put(Constants.REWRITE_URI, "/test");
+        exchange.getAttributes().put(Constants.RETRY_STRATEGY, 
RetryEnum.FAILOVER.getName());
+        exchange.getAttributes().put(Constants.DIVIDE_SELECTOR_ID, 
"selector-7475");
+        exchange.getAttributes().put(Constants.LOAD_BALANCE, "roundRobin");
+        return exchange;
+    }
+
+    private void assertFailoverExhausted(final Throwable error) {
+        ResponseStatusException statusException = 
assertInstanceOf(ResponseStatusException.class, error);
+        assertEquals(HttpStatus.SERVICE_UNAVAILABLE, 
statusException.getStatusCode());
+        assertEquals("CANNOT_FIND_HEALTHY_UPSTREAM_URL_AFTER_FAILOVER", 
statusException.getReason());
+    }
 }

Reply via email to