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) {