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 15fc782f2f fix(common): prevent fallback loops with encoded query 
parameters (#7449)
15fc782f2f is described below

commit 15fc782f2fbf44f7ac1be2a316de4e539c6a7caa
Author: lymerin <[email protected]>
AuthorDate: Thu Oct 8 14:28:04 2026 +0800

    fix(common): prevent fallback loops with encoded query parameters (#7449)
---
 .../org/apache/shenyu/common/utils/UriUtils.java   |  3 +-
 .../apache/shenyu/common/utils/UriUtilsTest.java   |  4 ++
 .../plugin/base/fallback/FallbackHandlerTest.java  | 43 ++++++++++++++++++++++
 3 files changed, 49 insertions(+), 1 deletion(-)

diff --git 
a/shenyu-common/src/main/java/org/apache/shenyu/common/utils/UriUtils.java 
b/shenyu-common/src/main/java/org/apache/shenyu/common/utils/UriUtils.java
index d5a65c69e4..9b347db9bf 100644
--- a/shenyu-common/src/main/java/org/apache/shenyu/common/utils/UriUtils.java
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/utils/UriUtils.java
@@ -84,7 +84,8 @@ public class UriUtils {
         if (Objects.isNull(uri)) {
             return StringUtils.EMPTY;
         }
-        String params = StringUtils.isEmpty(uri.getQuery()) ? "" : "?" + 
uri.getQuery();
+        // Preserve query encoding so fallback self-target checks compare raw 
URI values.
+        String params = StringUtils.isEmpty(uri.getRawQuery()) ? "" : "?" + 
uri.getRawQuery();
         return uri.getRawPath() + params;
     }
 
diff --git 
a/shenyu-common/src/test/java/org/apache/shenyu/common/utils/UriUtilsTest.java 
b/shenyu-common/src/test/java/org/apache/shenyu/common/utils/UriUtilsTest.java
index b4a686a8a7..663468a75f 100644
--- 
a/shenyu-common/src/test/java/org/apache/shenyu/common/utils/UriUtilsTest.java
+++ 
b/shenyu-common/src/test/java/org/apache/shenyu/common/utils/UriUtilsTest.java
@@ -85,6 +85,10 @@ public final class UriUtilsTest {
         assertNotNull(uri);
         ret = UriUtils.getPathWithParams(uri);
         assertEquals("/path?key=val", ret);
+
+        uri = UriUtils.createUri("https://example.com/path?key=a%26b";);
+        ret = UriUtils.getPathWithParams(uri);
+        assertEquals("/path?key=a%26b", ret);
     }
 
     @Test
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/fallback/FallbackHandlerTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/fallback/FallbackHandlerTest.java
index b810d5bff4..a1159d5713 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/fallback/FallbackHandlerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/fallback/FallbackHandlerTest.java
@@ -21,10 +21,14 @@ import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
 import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
 import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
 import org.springframework.web.reactive.DispatcherHandler;
@@ -35,9 +39,13 @@ import reactor.test.StepVerifier;
 import java.net.InetSocketAddress;
 import java.net.URI;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /**
@@ -95,6 +103,41 @@ public final class FallbackHandlerTest {
                 URI.create("fallback:/SHENYU/SHENYU"), 
mock(RuntimeException.class))).expectSubscription().verifyComplete());
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"/fallback?reason=a%26b", 
"fallback:/fallback?reason=a%26b"})
+    void fallbackToSameEncodedQuery(final String target) {
+        URI current = URI.create("http://localhost/fallback?reason=a%26b";);
+        MockServerWebExchange fallbackExchange = 
MockServerWebExchange.from(MockServerHttpRequest.method(HttpMethod.GET, 
current));
+        RuntimeException exception = new RuntimeException(new 
IllegalStateException("upstream failed"));
+        RuntimeException thrown = assertThrows(RuntimeException.class,
+                () -> 
testFallbackAvoidRedirectLoopHandler.fallback(fallbackExchange, 
URI.create(target), exception));
+        assertSame(exception.getCause(), thrown.getCause());
+    }
+
+    @Test
+    void relativeFallbackToDifferentQuery() {
+        URI current = URI.create("http://localhost/fallback?reason=a%26b";);
+        URI target = URI.create("/fallback?reason=a&b");
+        MockServerWebExchange fallbackExchange = 
MockServerWebExchange.from(MockServerHttpRequest.method(HttpMethod.GET, 
current));
+
+        
StepVerifier.create(testFallbackAvoidRedirectLoopHandler.fallback(fallbackExchange,
 target, new RuntimeException("upstream failed"))).verifyComplete();
+
+        assertEquals(HttpStatus.FOUND, 
fallbackExchange.getResponse().getStatusCode());
+        assertEquals(target, 
fallbackExchange.getResponse().getHeaders().getLocation());
+    }
+
+    @Test
+    void internalFallbackToDifferentQuery() {
+        URI current = URI.create("http://localhost/fallback?reason=a%26b";);
+        MockServerWebExchange fallbackExchange = 
MockServerWebExchange.from(MockServerHttpRequest.method(HttpMethod.GET, 
current));
+
+        
StepVerifier.create(testFallbackAvoidRedirectLoopHandler.fallback(fallbackExchange,
+                URI.create("fallback:/fallback?reason=a&b"), new 
RuntimeException("upstream failed"))).verifyComplete();
+
+        
verify(SpringBeanUtils.getInstance().getBean(DispatcherHandler.class)).handle(argThat(dispatched
 ->
+                
"http://localhost/fallback?reason=a&b".equals(dispatched.getRequest().getURI().toString())));
+    }
+
     static class TestFallbackHandler implements FallbackHandler {
         @Override
         public Mono<Void> withoutFallback(final ServerWebExchange exchange, 
final Throwable throwable) {

Reply via email to