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 0ce19b07c8 fix(ai): transform flushed response bodies (#7123)
0ce19b07c8 is described below

commit 0ce19b07c8e5b672f9767dbec7e3931595b8b14a
Author: Liming Deng <[email protected]>
AuthorDate: Sun Oct 11 23:18:45 2026 +0800

    fix(ai): transform flushed response bodies (#7123)
---
 .../response/AiResponseTransformerPlugin.java      | 13 +++++-
 .../response/AiResponseTransformerPluginTest.java  | 53 ++++++++++++++++++++++
 2 files changed, 64 insertions(+), 2 deletions(-)

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 c8db6f4419..71ccc28e6a 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
@@ -44,6 +44,7 @@ import org.springframework.http.codec.HttpMessageReader;
 import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
 import org.springframework.lang.NonNull;
 import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import org.reactivestreams.Publisher;
 
@@ -67,6 +68,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 
 /**
  * this is ai response transformer plugin.
+ * The complete response is buffered before transformation; streaming and SSE 
responses are not supported.
  */
 public class AiResponseTransformerPlugin extends AbstractShenyuPlugin {
 
@@ -305,7 +307,7 @@ public class AiResponseTransformerPlugin extends 
AbstractShenyuPlugin {
         @NonNull
         public Mono<Void> writeWith(@NonNull final Publisher<? extends 
DataBuffer> body) {
             final Mono<DataBuffer> dataBufferMono = DataBufferUtils.join(body);
-            return dataBufferMono.flatMap(dataBuffer -> {
+            return dataBufferMono.map(dataBuffer -> {
                 byte[] bytes = new byte[dataBuffer.readableByteCount()];
                 dataBuffer.read(bytes);
                 DataBufferUtils.release(dataBuffer);
@@ -375,7 +377,14 @@ public class AiResponseTransformerPlugin extends 
AbstractShenyuPlugin {
                                         return 
WebFluxResultUtils.result(this.exchange, 
finalResponseBody.getBytes(StandardCharsets.UTF_8));
                                     });
                         });
-            });
+            }).defaultIfEmpty(Mono.defer(() -> super.writeWith(Flux.empty())))
+                    .flatMap(response -> response);
+        }
+
+        @Override
+        @NonNull
+        public Mono<Void> writeAndFlushWith(@NonNull final Publisher<? extends 
Publisher<? extends DataBuffer>> body) {
+            return writeWith(Flux.from(body).concatMap(Flux::from));
         }
 
         private String extractBodyFromAiResponse(final String aiResponse) {
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPluginTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPluginTest.java
index df27629feb..63f1a802e4 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPluginTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/test/java/org/apache/shenyu/plugin/ai/transformer/response/AiResponseTransformerPluginTest.java
@@ -33,6 +33,8 @@ import org.apache.shenyu.plugin.api.result.ShenyuResult;
 import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
 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.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.MockedConstruction;
@@ -62,12 +64,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockConstruction;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.lenient;
 
@@ -180,6 +184,55 @@ class AiResponseTransformerPluginTest {
         }
     }
 
+    @ParameterizedTest
+    @ValueSource(ints = {0, 1, 2})
+    void testEmptyResponseIsCommitted(final int mode) {
+        MockServerHttpResponse response = (MockServerHttpResponse) 
exchange.getResponse();
+        response.setStatusCode(HttpStatus.NO_CONTENT);
+        response.getHeaders().set("X-Original", "retained");
+        AiResponseTransformerTemplate template = 
mock(AiResponseTransformerTemplate.class);
+        ChatClient client = mock(ChatClient.class);
+        AiResponseTransformerPlugin.AiResponseTransformerDecorator decorator =
+                new 
AiResponseTransformerPlugin.AiResponseTransformerDecorator(exchange, template, 
client);
+        Mono<Void> completion;
+        if (mode == 0) {
+            completion = decorator.writeWith(Flux.empty());
+        } else if (mode == 1) {
+            completion = decorator.writeAndFlushWith(Flux.empty());
+        } else {
+            completion = decorator.writeAndFlushWith(Flux.just(Flux.empty(), 
Flux.empty()));
+        }
+        StepVerifier.create(completion).verifyComplete();
+        assertTrue(response.isCommitted());
+        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
+        assertEquals("retained", response.getHeaders().getFirst("X-Original"));
+        verifyNoInteractions(template, client);
+    }
+
+    @Test
+    void testWriteAndFlushWithTransformsResponse() {
+        MockServerHttpResponse response = (MockServerHttpResponse) 
exchange.getResponse();
+        AiResponseTransformerTemplate template = 
mock(AiResponseTransformerTemplate.class);
+        
when(template.assembleMessage(exchange)).thenReturn(Mono.just("{\"response\":{\"body\":\"\"}}"));
+        ChatClient chatClient = mock(ChatClient.class, RETURNS_DEEP_STUBS);
+        when(chatClient.prompt().user(anyString()).stream().content())
+                .thenReturn(Flux.just("HTTP/1.1 200 OK\nContent-Type: 
application/json\n\n{\"transformed\":true}"));
+        ConfigurableApplicationContext context = 
mock(ConfigurableApplicationContext.class);
+        when(context.getBean(ShenyuResult.class)).thenReturn(new 
DefaultShenyuResult());
+        SpringBeanUtils.getInstance().setApplicationContext(context);
+        AiResponseTransformerPlugin.AiResponseTransformerDecorator decorator =
+                new 
AiResponseTransformerPlugin.AiResponseTransformerDecorator(exchange, template, 
chatClient);
+        DataBuffer first = 
response.bufferFactory().wrap("orig".getBytes(StandardCharsets.UTF_8));
+        DataBuffer second = 
response.bufferFactory().wrap("inal".getBytes(StandardCharsets.UTF_8));
+
+        
StepVerifier.create(decorator.writeAndFlushWith(Flux.just(Flux.just(first), 
Flux.just(second))))
+                .verifyComplete();
+
+        StepVerifier.create(response.getBodyAsString())
+                .expectNext("{\"transformed\":true}")
+                .verifyComplete();
+    }
+
     @Test
     void testOriginalHeadersRemainWhenTransformedBodyIsInvalid() {
         MockServerHttpResponse response = (MockServerHttpResponse) 
exchange.getResponse();

Reply via email to