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 7607a2bcc2 fix(ai): preserve JSON array bodies in request transformer 
(#7486)
7607a2bcc2 is described below

commit 7607a2bcc2f4d8e2ed74367068021f7e8fba369d
Author: lymerin <[email protected]>
AuthorDate: Thu Oct 8 14:27:50 2026 +0800

    fix(ai): preserve JSON array bodies in request transformer (#7486)
---
 .../request/AiRequestTransformerPlugin.java        |  5 +-
 .../request/AiRequestTransformerPluginTest.java    | 88 +++++++++++++---------
 2 files changed, 55 insertions(+), 38 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 0dc6b3bcfb..be6e0bdd92 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
@@ -17,6 +17,7 @@
 
 package org.apache.shenyu.plugin.ai.transformer.request;
 
+import com.google.gson.JsonElement;
 import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
 import org.apache.shenyu.common.dto.convert.plugin.AiRequestTransformerConfig;
@@ -197,8 +198,8 @@ public class AiRequestTransformerPlugin extends 
AbstractShenyuPlugin {
         String body = bodyBuilder.toString().trim();
 
         if (body.startsWith("{") && body.endsWith("}") || body.startsWith("[") 
&& body.endsWith("]")) {
-            Map<String, Object> requestBodyMap = 
GsonUtils.getInstance().convertToMap(body);
-            return GsonUtils.getInstance().toJson(requestBodyMap);
+            GsonUtils.getInstance().fromJson(body, JsonElement.class);
+            return body;
         }
 
         return null;
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 8c2366e96f..119e023338 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
@@ -17,31 +17,43 @@
 
 package org.apache.shenyu.plugin.ai.transformer.request;
 
+import com.google.gson.JsonSyntaxException;
 import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
-import org.apache.shenyu.common.dto.convert.plugin.AiRequestTransformerConfig;
 import org.apache.shenyu.common.dto.convert.rule.AiRequestTransformerHandle;
-import org.apache.shenyu.common.enums.AiModelProviderEnum;
-import org.apache.shenyu.plugin.ai.common.spring.ai.AiModelFactory;
 import 
org.apache.shenyu.plugin.ai.common.spring.ai.registry.AiModelFactoryRegistry;
 import org.apache.shenyu.plugin.ai.transformer.request.cache.ChatClientCache;
+import 
org.apache.shenyu.plugin.ai.transformer.request.handler.AiRequestTransformerPluginHandler;
 import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.base.utils.CacheKeyUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
 import org.springframework.ai.chat.client.ChatClient;
-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.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.reactive.function.server.HandlerStrategies;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
-import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -61,7 +73,7 @@ class AiRequestTransformerPluginTest {
         aiModelFactoryRegistry = mock(AiModelFactoryRegistry.class);
         chatClientCache = mock(ChatClientCache.class);
         chain = mock(ShenyuPluginChain.class);
-        plugin = new AiRequestTransformerPlugin(Collections.emptyList(), 
aiModelFactoryRegistry);
+        plugin = new 
AiRequestTransformerPlugin(HandlerStrategies.withDefaults().messageReaders(), 
aiModelFactoryRegistry);
     }
 
     @Test
@@ -81,49 +93,53 @@ class AiRequestTransformerPluginTest {
         verify(chain).execute(exchange);
     }
 
-    @Test
-    void testDoExecuteWithValidConfigurations() {
-
-        AiRequestTransformerConfig config = new AiRequestTransformerConfig();
-        config.setBaseUrl("http://test.com";);
-        config.setApiKey("test-api-key");
-        config.setProvider("TEST_PROVIDER");
-        config.setModel("test-model");
-
+    @ParameterizedTest
+    @ValueSource(strings = {
+            
"[{\"generated\":\"array\",\"nested\":[[1,2],true,null],\"number\":12345678901234567890},1,true,null]",
+            "[]",
+            "{\n  \"generated\": \"object\",\n  \"items\": [1,true],\n  
\"missing\": null\n}",
+            "{\"a\":[[1,2]]}"
+    })
+    void testDoExecuteWithValidConfigurations(final String body) {
         AiRequestTransformerHandle handle = new AiRequestTransformerHandle();
         handle.setProvider("TEST_PROVIDER");
         handle.setBaseUrl("http://test.com";);
         handle.setApiKey("test-api-key");
         handle.setModel("test-model");
 
-        ChatClient mockClient = mock(ChatClient.class);
-        ChatModel mockModel = mock(ChatModel.class);
-        AiModelFactory mockFactory = mock(AiModelFactory.class);
-
+        ChatClient mockClient = mock(ChatClient.class, RETURNS_DEEP_STUBS);
+        String aiResponse = "POST /test HTTP/1.1\nContent-Type: 
application/json\n\n" + body;
+        
when(mockClient.prompt().user(anyString()).stream().content()).thenReturn(Flux.just(aiResponse));
         MockServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/test")
                 .header(HttpHeaders.CONTENT_TYPE, 
MediaType.APPLICATION_JSON_VALUE)
-                .body("{\"key\":\"value\"}"));
-        
when(aiModelFactoryRegistry.getFactory(AiModelProviderEnum.getByName("TEST_PROVIDER"))).thenReturn(mockFactory);
-        when(mockFactory.createAiModel(any())).thenReturn(mockModel);
-        when(chatClientCache.getClient("default")).thenReturn(mockClient);
-        when(chain.execute(exchange)).thenReturn(Mono.empty());
-
+                .body("[{\"original\":true}]"));
         RuleData rule = new RuleData();
-        rule.setId("test-request-rule-id");
+        rule.setId("test-json-body-rule-id");
         rule.setSelectorId("test-selector-id");
-        SelectorData selector = new SelectorData();
-        StepVerifier.create(plugin.doExecute(exchange, chain, selector, rule))
-                .verifyComplete();
-
-        verify(chain).execute(exchange);
+        when(chatClientCache.getClient(rule.getId())).thenReturn(mockClient);
+        List<HttpMessageReader<?>> readers = 
HandlerStrategies.withDefaults().messageReaders();
+        AtomicReference<String> downstreamBody = new AtomicReference<>();
+        
when(chain.execute(any(ServerWebExchange.class))).thenAnswer(invocation ->
+                ServerRequest.create(invocation.getArgument(0), 
readers).bodyToMono(String.class)
+                        .doOnNext(downstreamBody::set).then());
+
+        String cacheKey = CacheKeyUtils.INST.getKey(rule);
+        
AiRequestTransformerPluginHandler.CACHED_HANDLE.get().cachedHandle(cacheKey, 
handle);
+        try (MockedStatic<ChatClientCache> mockedCache = 
mockStatic(ChatClientCache.class)) {
+            
mockedCache.when(ChatClientCache::getInstance).thenReturn(chatClientCache);
+            StepVerifier.create(plugin.doExecute(exchange, chain, new 
SelectorData(), rule)).verifyComplete();
+
+            assertEquals(body, downstreamBody.get());
+            verify(chain).execute(any(ServerWebExchange.class));
+        } finally {
+            
AiRequestTransformerPluginHandler.CACHED_HANDLE.get().removeHandle(cacheKey);
+        }
     }
 
     @Test
-    void testConvertBodyJson() {
-
-        String aiResponse = "HTTP/1.1 / 200 OK\nContent-Type: 
application/json\n\n{\"key\":\"value\"}";
-        String result = AiRequestTransformerPlugin.convertBodyJson(aiResponse);
-        assertEquals("{\"key\":\"value\"}", result);
+    void testConvertBodyJsonWithMalformedJson() {
+        String aiResponse = "POST /test HTTP/1.1\nContent-Type: 
application/json\n\n[{\"broken\":}]";
+        assertThrows(JsonSyntaxException.class, () -> 
AiRequestTransformerPlugin.convertBodyJson(aiResponse));
     }
 
     @Test

Reply via email to