[type: BUG] fix fallback issue (#5496)

* [ISSUE #5494] fix fallback issue

* [ISSUE #5494]
* avoid redirect loop
* add unit test

---------

Co-authored-by: ray <zhongzhenchao@ingbaobei.com>
Co-authored-by: moremind <hefengen@apache.org>
Co-authored-by: xiaoyu <xiaoyu@apache.org>
Co-authored-by: loongs-zhang <zhangzicheng@apache.org>
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 3a1fde3..6adf4df 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
@@ -43,6 +43,18 @@
     }
 
     /**
+     * create URI {@link URI}.
+     *
+     * @param scheme    scheme eg:http
+     * @param authority registry or server eg: 127.0.0.1:8080
+     * @param path      path eg:/ fallback
+     * @return created {@link URI} from uri
+     */
+    public static URI createUri(final String scheme, final String authority, final String path) {
+        return createUri(scheme + "://" + authority + repairData(path));
+    }
+
+    /**
      * Repair data string.
      *
      * @param name the name
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 3047e68..b4a686a 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
@@ -41,6 +41,9 @@
 
         uri = UriUtils.createUri("");
         assertNull(uri);
+
+        uri = UriUtils.createUri("https", "example.com", "/http");
+        assertEquals("https://example.com/http", uri.toString());
     }
 
     @Test
diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/fallback/FallbackHandler.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/fallback/FallbackHandler.java
index 5d4aeab..cfd4c93 100644
--- a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/fallback/FallbackHandler.java
+++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/fallback/FallbackHandler.java
@@ -58,12 +58,18 @@
         // client HttpStatusCodeException, return the client response directly
         if (t instanceof HttpStatusCodeException || Objects.isNull(uri)) {
             return withoutFallback(exchange, t);
-        } 
+        }
         if (uri.toString().startsWith(PREFIX)) {
-            String fallbackUri = uri.toString().substring(PREFIX.length());
+            String fallbackPath = uri.toString().substring(PREFIX.length());
             DispatcherHandler dispatcherHandler =
                     SpringBeanUtils.getInstance().getBean(DispatcherHandler.class);
-            ServerHttpRequest request = exchange.getRequest().mutate().uri(URI.create(fallbackUri)).build();
+            URI previsouUri = exchange.getRequest().getURI();
+            // avoid redirect loop, return error.
+            if (UriUtils.getPathWithParams(previsouUri).equals(fallbackPath)) {
+                return withoutFallback(exchange, t);
+            }
+            URI fallbackUri = UriUtils.createUri(previsouUri.getScheme(), previsouUri.getAuthority(), fallbackPath);
+            ServerHttpRequest request = exchange.getRequest().mutate().uri(fallbackUri).build();
             ServerWebExchange mutated = exchange.mutate().request(request).build();
             return dispatcherHandler.handle(mutated);
         }
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 9b3de9a..b810d5b 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
@@ -35,6 +35,7 @@
 import java.net.InetSocketAddress;
 import java.net.URI;
 
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -50,6 +51,8 @@
 
     private TestFallbackHandler testFallbackHandler;
 
+    private TestFallbackAvoidRedirectLoopHandler testFallbackAvoidRedirectLoopHandler;
+
     @BeforeEach
     public void setUp() {
         ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
@@ -62,6 +65,7 @@
                 .build());
         when(handler.handle(any())).thenReturn(Mono.empty());
         this.testFallbackHandler = new TestFallbackHandler();
+        this.testFallbackAvoidRedirectLoopHandler = new TestFallbackAvoidRedirectLoopHandler();
     }
 
     /**
@@ -76,15 +80,32 @@
      * The fallback test.
      */
     @Test
-    public void fallbackTest() {
+    public void httpFallbackPrefixTest() {
         StepVerifier.create(testFallbackHandler.fallback(exchange, null, mock(RuntimeException.class))).expectSubscription().verifyComplete();
         StepVerifier.create(testFallbackHandler.fallback(exchange, URI.create("http://127.0.0.1:8090/SHENYU"), mock(RuntimeException.class))).expectSubscription().verifyComplete();
     }
 
+    /**
+     * The fallback test.
+     */
+    @Test
+    public void fallbackPrefixTest() {
+        StepVerifier.create(testFallbackHandler.fallback(exchange, URI.create("fallback:/SHENYU"), mock(RuntimeException.class))).expectSubscription().verifyComplete();
+        assertThrows(RuntimeException.class, () -> StepVerifier.create(testFallbackAvoidRedirectLoopHandler.fallback(exchange,
+                URI.create("fallback:/SHENYU/SHENYU"), mock(RuntimeException.class))).expectSubscription().verifyComplete());
+    }
+
     static class TestFallbackHandler implements FallbackHandler {
         @Override
         public Mono<Void> withoutFallback(final ServerWebExchange exchange, final Throwable throwable) {
             return Mono.empty();
         }
     }
+
+    static class TestFallbackAvoidRedirectLoopHandler implements FallbackHandler {
+        @Override
+        public Mono<Void> withoutFallback(final ServerWebExchange exchange, final Throwable throwable) {
+            throw new RuntimeException(throwable.getCause());
+        }
+    }
 }