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

liuhongyu 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 205fe32a3c feat: refactor aiRequest and aiResponse models to support 
asynchronou… (#6296)
205fe32a3c is described below

commit 205fe32a3c06b654dcd03529c39c980b105a4492
Author: Yu Siheng <[email protected]>
AuthorDate: Sun Feb 15 22:32:01 2026 +0800

    feat: refactor aiRequest and aiResponse models to support asynchronou… 
(#6296)
    
    * feat: refactor aiRequest and aiResponse models to support asynchronous 
operations
    
    * add:logging and illegal judgment
    
    * fix
---
 .../request/AiRequestTransformerPlugin.java        | 89 ++++++++++++++++++++--
 .../template/AiRequestTransformerTemplate.java     |  7 ++
 .../request/AiRequestTransformerPluginTest.java    | 12 ++-
 .../response/AiResponseTransformerPlugin.java      |  9 ++-
 4 files changed, 103 insertions(+), 14 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPlugin.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPlugin.java
index 7ba8480c4e..0dc6b3bcfb 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPlugin.java
@@ -41,19 +41,21 @@ import org.springframework.ai.chat.model.ChatModel;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.codec.HttpMessageReader;
+import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Schedulers;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
+import java.net.URI;
 import java.net.URLEncoder;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 /**
@@ -115,15 +117,25 @@ public class AiRequestTransformerPlugin extends 
AbstractShenyuPlugin {
 
         AiRequestTransformerTemplate aiRequestTransformerTemplate = new 
AiRequestTransformerTemplate(aiRequestTransformerConfig.getContent(), 
exchange.getRequest());
         ChatClient finalClient = client;
+
         return aiRequestTransformerTemplate.assembleMessage()
-                .flatMap(message -> Mono.fromCallable(() -> 
finalClient.prompt().user(message).call().content())
-                        .subscribeOn(Schedulers.boundedElastic())
-                        .flatMap(aiResponse -> convertHeader(exchange, 
aiResponse)
-                                .flatMap(serverWebExchange -> 
convertBody(serverWebExchange, messageReaders, aiResponse))
-                                    .flatMap(chain::execute)
+                .flatMap(message -> 
finalClient.prompt().user(message).stream().content()
+                        .collectList()
+                        .map(list -> Objects.isNull(list) ? "" : 
list.stream().filter(Objects::nonNull)
+                                .collect(Collectors.joining("")))
+                        .flatMap(aiResponse -> {
+                            LOG.debug("Request rewritten to: {}", aiResponse);
+                            return convertHeader(exchange, aiResponse)
+                                    .flatMap(serverWebExchange -> 
convertBody(serverWebExchange, messageReaders, aiResponse))
+                                    .flatMap(serverWebExchange -> 
rewriteRequestPath(serverWebExchange, aiResponse))
+                                    .flatMap(chain::execute);
+                                }
                         )
-                );
-
+                )
+                .onErrorResume(throwable -> {
+                    LOG.error("AI request transformation failed, proceeding 
without AI transformation", throwable);
+                    return chain.execute(exchange);
+                });
     }
 
     private static Mono<ServerWebExchange> convertBody(final ServerWebExchange 
exchange,
@@ -218,6 +230,46 @@ public class AiRequestTransformerPlugin extends 
AbstractShenyuPlugin {
         return Mono.just(exchange);
     }
 
+    private static Mono<ServerWebExchange> rewriteRequestPath(final 
ServerWebExchange exchange, final String aiResponse) {
+        String newPath = extractRequestPathFromAiResponse(aiResponse);
+
+        if (Objects.isNull(newPath) || newPath.isEmpty()) {
+            return Mono.just(exchange);
+        }
+
+        if (newPath.contains("..")) {
+            LOG.warn("Detected potential path traversal attempt in extracted 
path: {} , Will continue to use the original path.", newPath);
+            return Mono.just(exchange);
+        }
+
+        if (!newPath.startsWith("/")) {
+            LOG.warn("Extracted path does not start with '/': {} , Will 
continue to use the original path.", newPath);
+            return Mono.just(exchange);
+        }
+
+        if (!newPath.matches("^/[a-zA-Z0-9/_\\-]*$")) {
+            LOG.warn("Extracted path contains invalid characters: {}, Will 
continue to use the original path.", newPath);
+            return Mono.just(exchange);
+        }
+
+        LOG.debug("Request path after validation and rewriting: {}", newPath);
+
+        ServerHttpRequest originalRequest = exchange.getRequest();
+        URI originalUri = originalRequest.getURI();
+
+        URI newUri = originalUri.resolve(newPath);
+
+        ServerHttpRequest newRequest = originalRequest.mutate()
+                .uri(newUri)
+                .build();
+
+        ServerWebExchange newExchange = exchange.mutate()
+                .request(newRequest)
+                .build();
+
+        return Mono.just(newExchange);
+    }
+
     /**
      * For unit test.
      */
@@ -253,6 +305,27 @@ public class AiRequestTransformerPlugin extends 
AbstractShenyuPlugin {
         return headers;
     }
 
+    /**
+     * For unit test.
+     */
+    static String extractRequestPathFromAiResponse(final String aiResponse) {
+        if (Objects.isNull(aiResponse) || aiResponse.isEmpty()) {
+            return null;
+        }
+        String[] lines = aiResponse.split("\r?\n");
+        if (lines.length == 0) {
+            return null;
+        }
+
+        String startLine = lines[0].trim();
+        String[] parts = startLine.split(" ");
+        if (parts.length < 2) {
+            return null;
+        }
+
+        return parts[1];
+    }
+
     @Override
     public int getOrder() {
         return PluginEnum.AI_REQUEST_TRANSFORMER.getCode();
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/template/AiRequestTransformerTemplate.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/template/AiRequestTransformerTemplate.java
index 320ebed87b..a956ff0ced 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/template/AiRequestTransformerTemplate.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/request/template/AiRequestTransformerTemplate.java
@@ -156,7 +156,14 @@ public class AiRequestTransformerTemplate {
 
                     ObjectNode requestNode = objectMapper.createObjectNode();
                     requestNode.set("headers", headersJson);
+                    String fullPath = originalRequest.getURI().getRawPath();
+                    String query = originalRequest.getURI().getRawQuery();
 
+                    if (Objects.nonNull(query)) {
+                        fullPath += "?" + query;
+                    }
+
+                    requestNode.put("path", fullPath);
                     if (Objects.nonNull(contentType)) {
                         if 
(MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
                             try {
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPluginTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPluginTest.java
index 0d56a48fe0..066121b2b4 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPluginTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-request-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/request/AiRequestTransformerPluginTest.java
@@ -117,7 +117,7 @@ class AiRequestTransformerPluginTest {
     @Test
     void testConvertBodyJson() {
 
-        String aiResponse = "HTTP/1.1 200 OK\nContent-Type: 
application/json\n\n{\"key\":\"value\"}";
+        String aiResponse = "HTTP/1.1 / 200 OK\nContent-Type: 
application/json\n\n{\"key\":\"value\"}";
         String result = AiRequestTransformerPlugin.convertBodyJson(aiResponse);
         assertEquals("{\"key\":\"value\"}", result);
     }
@@ -125,9 +125,17 @@ class AiRequestTransformerPluginTest {
     @Test
     void testExtractHeadersFromAiResponse() {
 
-        String aiResponse = "HTTP/1.1 200 OK\nContent-Type: 
application/json\nAuthorization: Bearer token\n\n{\"key\":\"value\"}";
+        String aiResponse = "HTTP/1.1 / 200 OK\nContent-Type: 
application/json\nAuthorization: Bearer token\n\n{\"key\":\"value\"}";
         HttpHeaders headers = 
AiRequestTransformerPlugin.extractHeadersFromAiResponse(aiResponse);
         assertEquals("application/json", 
headers.getFirst(HttpHeaders.CONTENT_TYPE));
         assertEquals("Bearer token", 
headers.getFirst(HttpHeaders.AUTHORIZATION));
     }
+
+    @Test
+    void testRewriteRequestPath() {
+
+        String aiResponse = "HTTP/1.1 / 200 OK\nContent-Type: 
application/json\nAuthorization: Bearer token\n\n{\"key\":\"value\"}";
+        String result = 
AiRequestTransformerPlugin.extractRequestPathFromAiResponse(aiResponse);
+        assertEquals("/", result);
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPlugin.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPlugin.java
index 0c274cf2a4..033ee402fd 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPlugin.java
@@ -45,7 +45,6 @@ import 
org.springframework.http.server.reactive.ServerHttpResponseDecorator;
 import org.springframework.lang.NonNull;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Schedulers;
 import org.reactivestreams.Publisher;
 
 import java.io.BufferedReader;
@@ -56,6 +55,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -350,9 +350,10 @@ public class AiResponseTransformerPlugin extends 
AbstractShenyuPlugin {
                             }
                             
                             final String finalMessage = 
messageWithResponseBody;
-                            
-                            return Mono.fromCallable(() -> 
chatClient.prompt().user(finalMessage).call().content())
-                                    .subscribeOn(Schedulers.boundedElastic())
+                            return 
chatClient.prompt().user(finalMessage).stream().content()
+                                    .collectList()
+                                    .map(list -> Objects.isNull(list) ? "" : 
list.stream().filter(Objects::nonNull)
+                                            .collect(Collectors.joining("")))
                                     .flatMap(aiResponse -> {
 
                                         HttpHeaders newHeaders = 
extractHeadersFromAiResponse(aiResponse);

Reply via email to