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