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 00322546ce fix(ai): prevent prompt configuration leaking across 
requests (#7488)
00322546ce is described below

commit 00322546ce4f5fb40913352b7fa7668e0c30276c
Author: lymerin <[email protected]>
AuthorDate: Thu Oct 8 14:28:40 2026 +0800

    fix(ai): prevent prompt configuration leaking across requests (#7488)
---
 .../shenyu/plugin/ai/prompt/AiPromptPlugin.java    |  1 +
 .../plugin/ai/prompt/AiPromptPluginTest.java       | 58 ++++++++++++++++++++++
 2 files changed, 59 insertions(+)

diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/main/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPlugin.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/main/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPlugin.java
index 9e38002748..4b7db4df8d 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/main/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/main/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPlugin.java
@@ -69,6 +69,7 @@ public class AiPromptPlugin extends AbstractShenyuPlugin {
         
         // Create final config with selector handle taking precedence
         if (Objects.nonNull(aiPromptHandle)) {
+            aiPromptConfig = new AiPromptConfig();
             aiPromptConfig.setPreRole(aiPromptHandle.getPreRole());
             aiPromptConfig.setPrepend(aiPromptHandle.getPrepend());
             aiPromptConfig.setPostRole(aiPromptHandle.getPostRole());
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/test/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPluginTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/test/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPluginTest.java
index f4b57e69c4..6405124dd6 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/test/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPluginTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-prompt/src/test/java/org/apache/shenyu/plugin/ai/prompt/AiPromptPluginTest.java
@@ -17,16 +17,30 @@
 
 package org.apache.shenyu.plugin.ai.prompt;
 
+import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.convert.plugin.AiPromptConfig;
 import org.apache.shenyu.common.enums.PluginEnum;
 import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.Singleton;
+import org.apache.shenyu.plugin.ai.prompt.handler.AiPromptPluginDataHandler;
+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.springframework.http.MediaType;
 import org.springframework.http.codec.HttpMessageReader;
+import org.springframework.http.codec.ServerCodecConfigurer;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import reactor.test.StepVerifier;
 
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.mock;
@@ -55,6 +69,44 @@ class AiPromptPluginTest {
         assertEquals(PluginEnum.AI_PROMPT.getCode(), plugin.getOrder());
     }
 
+    @Test
+    void testRuleConfigDoesNotMutatePluginConfig() throws Exception {
+        Field singlesField = Singleton.class.getDeclaredField("SINGLES");
+        singlesField.setAccessible(true);
+        Map<?, ?> singles = (Map<?, ?>) singlesField.get(null);
+        AiPromptConfig previousConfig = 
Singleton.INST.get(AiPromptConfig.class);
+        String baseConfigJson = "{\"prepend\":\"base 
prefix\",\"preRole\":\"system\","
+                + "\"append\":\"base suffix\",\"postRole\":\"assistant\"}";
+        AiPromptConfig baseConfig = 
GsonUtils.getInstance().fromJson(baseConfigJson, AiPromptConfig.class);
+        RuleData ruleA = 
RuleData.builder().selectorId("prompt-isolation").id("a")
+                
.handle("{\"prepend\":\"rule-A\",\"preRole\":\"developer\"}").build();
+        RuleData ruleB = 
RuleData.builder().selectorId("prompt-isolation").id("b").build();
+        AiPromptPluginDataHandler handler = new AiPromptPluginDataHandler();
+        List<HttpMessageReader<?>> readers = 
ServerCodecConfigurer.create().getReaders();
+        AiPromptPlugin requestPlugin = new AiPromptPlugin(readers);
+        List<Object> results = new ArrayList<>();
+        ShenyuPluginChain chain = exchange -> ServerRequest.create(exchange, 
readers).bodyToMono(String.class)
+                .doOnNext(body -> 
results.add(GsonUtils.getInstance().convertToMap(body).get("messages"))).then();
+        try {
+            Singleton.INST.single(AiPromptConfig.class, baseConfig);
+            handler.handlerRule(ruleA);
+            Map<String, String> originalMessage = Map.of("role", "user", 
"content", "hello");
+            StepVerifier.create(requestPlugin.doExecute(promptExchange(), 
chain, null, ruleA)).verifyComplete();
+            assertEquals(List.of(Map.of("role", "developer", "content", 
"rule-A"), originalMessage), results.get(0));
+            assertEquals(GsonUtils.getInstance().fromJson(baseConfigJson, 
AiPromptConfig.class), Singleton.INST.get(AiPromptConfig.class));
+            StepVerifier.create(requestPlugin.doExecute(promptExchange(), 
chain, null, ruleB)).verifyComplete();
+            assertEquals(List.of(Map.of("role", "system", "content", "base 
prefix"), originalMessage,
+                    Map.of("role", "assistant", "content", "base suffix")), 
results.get(1));
+        } finally {
+            
AiPromptPluginDataHandler.CACHED_HANDLE.get().removeHandle(CacheKeyUtils.INST.getKey(ruleA));
+            if (Objects.isNull(previousConfig)) {
+                singles.remove(AiPromptConfig.class.getName());
+            } else {
+                Singleton.INST.single(AiPromptConfig.class, previousConfig);
+            }
+        }
+    }
+
     @Test
     void testDecorateBodyReturnsOriginalWhenNoMessages() throws Exception {
         AiPromptConfig config = new AiPromptConfig();
@@ -191,6 +243,12 @@ class AiPromptPluginTest {
         assertEquals(2, messages.size());
     }
 
+    private MockServerWebExchange promptExchange() {
+        return 
MockServerWebExchange.from(MockServerHttpRequest.post("/ai/prompt")
+                .contentType(MediaType.APPLICATION_JSON)
+                
.body("{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}"));
+    }
+
     private String invokeDecorateBody(final String body, final AiPromptConfig 
config) throws Exception {
         Method method = AiPromptPlugin.class.getDeclaredMethod("decorateBody", 
String.class, AiPromptConfig.class);
         method.setAccessible(true);

Reply via email to