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