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 be34dd1e7d fix(ai): apply the generated HTTP status in the AI response
transformer (#7915)
be34dd1e7d is described below
commit be34dd1e7d37a44d3d2badf3f307410832cfb521
Author: hanzhe-one <[email protected]>
AuthorDate: Wed Oct 21 20:58:48 2026 +0800
fix(ai): apply the generated HTTP status in the AI response transformer
(#7915)
The response-transformer system prompt asks the model for a complete
HTTP/1.1 response including the status line, but the decorator only
applied the generated headers and body. The generated status was parsed
by nothing and silently discarded, so a model answering
"HTTP/1.1 404 Not Found" still produced the upstream 200.
Parse the status line before the response is committed and apply it:
- The status line must be the first non-empty line of the model output
(after stripping markdown fences); a status-looking header value
further down cannot change the status.
- The code must be one the HTTP specification defines, otherwise the
generated status is ignored and the upstream status is retained.
- 204/304 are applied without a body, dropping the generated
Content-Type/Content-Length/Transfer-Encoding headers.
- Omitted or invalid output, and the no-usable-body fallback, keep the
upstream status.
Also accept HTTP/1.0 status lines when extracting headers and document
in the prompt that the status line is applied.
Fixes #7907
---
.../response/AiResponseTransformerPlugin.java | 148 ++++++++++++++++-----
.../template/AiResponseTransformerTemplate.java | 2 +
.../response/AiResponseTransformerPluginTest.java | 147 ++++++++++++++++++++
3 files changed, 262 insertions(+), 35 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 492a7bf4e3..97aea95529 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
@@ -40,6 +40,7 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.lang.NonNull;
@@ -58,6 +59,7 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
@@ -76,6 +78,8 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
private static final Pattern REQUEST_LINE_PATTERN =
Pattern.compile("^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\\s.*\\sHTTP/1.1$");
+ private static final Pattern STATUS_LINE_PATTERN =
Pattern.compile("^HTTP/1\\.[01]\\s+(\\d{3})(?:\\s+.*)?$");
+
private static final ObjectMapper MAPPER = new ObjectMapper();
private final List<HttpMessageReader<?>> messageReaders;
@@ -168,32 +172,53 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
return AiResponseParser.extractHeadersFromAiResponse(aiResponse);
}
+ /**
+ * For unit test.
+ *
+ * @param aiResponse the AI response
+ * @return the extracted status code, or {@code null} when the response
has no usable status line
+ */
+ public static Integer extractStatusFromAiResponse(final String aiResponse)
{
+ return AiResponseParser.extractStatusFromAiResponse(aiResponse);
+ }
+
/**
* Utility class for parsing AI responses.
*/
private static class AiResponseParser {
-
+
/**
- * Extract body from AI response.
+ * Remove the surrounding markdown code block markers, if any.
*
* @param aiResponse the AI response
- * @return the extracted body
+ * @return the response without code block markers
*/
- static String extractBodyFromAiResponse(final String aiResponse) {
- if (Objects.isNull(aiResponse) || aiResponse.isEmpty()) {
- return null;
- }
-
+ static String stripCodeBlockMarkers(final String aiResponse) {
String cleanedResponse = aiResponse;
if (cleanedResponse.contains("```")) {
int startIndex = cleanedResponse.indexOf("```");
int endIndex = cleanedResponse.lastIndexOf("```");
-
+
if (startIndex != -1 && endIndex != -1 && endIndex >
startIndex) {
cleanedResponse = cleanedResponse.substring(startIndex +
3, endIndex).trim();
LOG.debug("Removed code block markers, cleaned response:
{}", cleanedResponse);
}
}
+ return cleanedResponse;
+ }
+
+ /**
+ * Extract body from AI response.
+ *
+ * @param aiResponse the AI response
+ * @return the extracted body
+ */
+ static String extractBodyFromAiResponse(final String aiResponse) {
+ if (Objects.isNull(aiResponse) || aiResponse.isEmpty()) {
+ return null;
+ }
+
+ String cleanedResponse = stripCodeBlockMarkers(aiResponse);
if (cleanedResponse.startsWith("{") &&
cleanedResponse.endsWith("}")
|| cleanedResponse.startsWith("[") &&
cleanedResponse.endsWith("]")) {
@@ -249,23 +274,14 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
return headers;
}
- String cleanedResponse = aiResponse;
- if (cleanedResponse.contains("```")) {
- int startIndex = cleanedResponse.indexOf("```");
- int endIndex = cleanedResponse.lastIndexOf("```");
-
- if (startIndex != -1 && endIndex != -1 && endIndex >
startIndex) {
- cleanedResponse = cleanedResponse.substring(startIndex +
3, endIndex).trim();
- LOG.debug("Removed code block markers for header
extraction, cleaned response: {}", cleanedResponse);
- }
- }
+ String cleanedResponse = stripCodeBlockMarkers(aiResponse);
try (BufferedReader reader = new BufferedReader(new
StringReader(cleanedResponse))) {
String line;
boolean headerSectionStarted = false;
while (Objects.nonNull(line = reader.readLine())) {
if (!headerSectionStarted) {
- if (line.startsWith("HTTP/1.1") ||
REQUEST_LINE_PATTERN.matcher(line).matches()) {
+ if (STATUS_LINE_PATTERN.matcher(line.trim()).matches()
|| REQUEST_LINE_PATTERN.matcher(line.trim()).matches()) {
headerSectionStarted = true;
continue;
}
@@ -286,6 +302,44 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
}
return headers;
}
+
+ /**
+ * Extract the status code from the status line of an AI response.
+ * The status line is only accepted as the first non-empty line, so a
status-looking
+ * header value further down cannot change the response status.
+ *
+ * @param aiResponse the AI response
+ * @return the extracted status code, or {@code null} when there is no
valid status line
+ */
+ static Integer extractStatusFromAiResponse(final String aiResponse) {
+ if (Objects.isNull(aiResponse) || aiResponse.isEmpty()) {
+ return null;
+ }
+
+ String cleanedResponse = stripCodeBlockMarkers(aiResponse);
+ try (BufferedReader reader = new BufferedReader(new
StringReader(cleanedResponse))) {
+ String line;
+ while (Objects.nonNull(line = reader.readLine())) {
+ if (line.trim().isEmpty()) {
+ continue;
+ }
+ Matcher matcher = STATUS_LINE_PATTERN.matcher(line.trim());
+ if (!matcher.matches()) {
+ return null;
+ }
+ int statusCode = Integer.parseInt(matcher.group(1));
+ // HttpStatus resolves only codes defined by the HTTP
specification.
+ if (Objects.isNull(HttpStatus.resolve(statusCode))) {
+ LOG.warn("AI response contains an invalid HTTP status
code: {}", statusCode);
+ return null;
+ }
+ return statusCode;
+ }
+ } catch (IOException | NumberFormatException e) {
+ LOG.warn("AI response transformer plugin: extract status from
AiResponse fail");
+ }
+ return null;
+ }
}
static class AiResponseTransformerDecorator extends
ServerHttpResponseDecorator {
@@ -358,21 +412,7 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
.filter(Objects::nonNull)
.collect(StringBuilder::new,
StringBuilder::append)
.map(StringBuilder::toString)
- .flatMap(aiResponse -> {
-
- HttpHeaders newHeaders =
extractHeadersFromAiResponse(aiResponse);
- String newBody =
extractBodyFromAiResponse(aiResponse);
-
- if (Objects.nonNull(newBody) &&
!newBody.isEmpty()) {
- this.getHeaders().clear();
-
this.getHeaders().putAll(newHeaders);
- LOG.debug("Returning transformed
response body: {}", newBody);
- return
WebFluxResultUtils.result(this.exchange,
newBody.getBytes(StandardCharsets.UTF_8));
- } else {
- LOG.warn("response body is empty
or null, returning original response: {}", finalResponseBody);
- return
WebFluxResultUtils.result(this.exchange,
finalResponseBody.getBytes(StandardCharsets.UTF_8));
- }
- })
+ .flatMap(aiResponse ->
applyTransformedResponse(aiResponse, finalResponseBody))
.onErrorResume(throwable -> {
LOG.error("response transformation
failed", throwable);
LOG.info("Returning original response
due to error: {}", finalResponseBody);
@@ -389,6 +429,36 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
return writeWith(Flux.from(body).concatMap(Flux::from));
}
+ private Mono<Void> applyTransformedResponse(final String aiResponse,
final String originalResponseBody) {
+ HttpHeaders newHeaders = extractHeadersFromAiResponse(aiResponse);
+ String newBody = extractBodyFromAiResponse(aiResponse);
+ Integer newStatus = extractStatusFromAiResponse(aiResponse);
+
+ if (isBodylessStatus(newStatus)) {
+ // 204/304 must not carry a body, so the generated status and
headers are applied as-is
+ this.getHeaders().clear();
+ this.getHeaders().putAll(newHeaders);
+ this.getHeaders().remove(HttpHeaders.CONTENT_TYPE);
+ this.getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
+ this.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
+ this.setStatusCode(HttpStatus.valueOf(newStatus));
+ LOG.debug("Returning transformed bodyless response with
status: {}", newStatus);
+ return super.writeWith(Flux.empty());
+ }
+ if (Objects.nonNull(newBody) && !newBody.isEmpty()) {
+ this.getHeaders().clear();
+ this.getHeaders().putAll(newHeaders);
+ if (Objects.nonNull(newStatus)) {
+ this.setStatusCode(HttpStatus.valueOf(newStatus));
+ }
+ LOG.debug("Returning transformed response body: {}", newBody);
+ return WebFluxResultUtils.result(this.exchange,
newBody.getBytes(StandardCharsets.UTF_8));
+ }
+ // the generated response has no usable body, so the original
response (and its status) is kept
+ LOG.warn("response body is empty or null, returning original
response: {}", originalResponseBody);
+ return WebFluxResultUtils.result(this.exchange,
originalResponseBody.getBytes(StandardCharsets.UTF_8));
+ }
+
private String extractBodyFromAiResponse(final String aiResponse) {
if (Objects.isNull(aiResponse) || aiResponse.isEmpty()) {
LOG.warn("response is null or empty");
@@ -404,6 +474,14 @@ public class AiResponseTransformerPlugin extends
AbstractShenyuPlugin {
return result;
}
+ private static boolean isBodylessStatus(final Integer statusCode) {
+ if (Objects.isNull(statusCode)) {
+ return false;
+ }
+ HttpStatus status = HttpStatus.resolve(statusCode);
+ return HttpStatus.NO_CONTENT.equals(status) ||
HttpStatus.NOT_MODIFIED.equals(status);
+ }
+
private HttpHeaders extractHeadersFromAiResponse(final String
aiResponse) {
if (Objects.isNull(aiResponse) || aiResponse.isEmpty()) {
LOG.warn("AI response is null or empty for header extraction");
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/template/AiResponseTransformerTemplate.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/template/AiResponseTransformerTemplate.java
index f9119e688c..2ec3b9593b 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/template/AiResponseTransformerTemplate.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-response-transformer/src/main/java/org/apache/shenyu/plugin/ai/transformer/response/template/AiResponseTransformerTemplate.java
@@ -56,6 +56,8 @@ public class AiResponseTransformerTemplate {
+ "- A blank line \n"
+ "- A JSON-formatted body (the body must be valid JSON text) \n"
+ "Do not include any extra explanations, comments, or text. \n"
+ + "The status line is applied to the response, so only change the
status when the transformation requires it; "
+ + "otherwise keep the upstream status. Do not send a body with 204
or 304. \n"
+ "Example:\n"
+ "\n"
+ "HTTP/1.1 200 OK\n"
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 63f1a802e4..7f68bdd52d 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
@@ -233,6 +233,114 @@ class AiResponseTransformerPluginTest {
.verifyComplete();
}
+ @Test
+ void testGeneratedStatusIsApplied() {
+ MockServerHttpResponse response = (MockServerHttpResponse)
exchange.getResponse();
+ response.setStatusCode(HttpStatus.OK);
+ 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 404 Not Found\nContent-Type:
application/json\n\n{\"error\":\"missing\"}"));
+ 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 responseBody =
response.bufferFactory().wrap("original".getBytes(StandardCharsets.UTF_8));
+
+
StepVerifier.create(decorator.writeWith(Mono.just(responseBody))).verifyComplete();
+
+ assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
+ assertEquals("{\"error\":\"missing\"}",
response.getBodyAsString().block());
+ }
+
+ @Test
+ void testGeneratedSuccessStatusReplacesErrorStatus() {
+ MockServerHttpResponse response = (MockServerHttpResponse)
exchange.getResponse();
+ response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
+ 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 201 Created\nContent-Type:
application/json\n\n{\"created\":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 responseBody =
response.bufferFactory().wrap("failed".getBytes(StandardCharsets.UTF_8));
+
+
StepVerifier.create(decorator.writeWith(Mono.just(responseBody))).verifyComplete();
+
+ assertEquals(HttpStatus.CREATED, response.getStatusCode());
+ assertEquals("{\"created\":true}", response.getBodyAsString().block());
+ }
+
+ @Test
+ void testUpstreamStatusRetainedWhenGeneratedStatusIsInvalid() {
+ MockServerHttpResponse response = (MockServerHttpResponse)
exchange.getResponse();
+ response.setStatusCode(HttpStatus.ACCEPTED);
+ 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 999 Not Real\nContent-Type:
application/json\n\n{\"ok\":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 responseBody =
response.bufferFactory().wrap("original".getBytes(StandardCharsets.UTF_8));
+
+
StepVerifier.create(decorator.writeWith(Mono.just(responseBody))).verifyComplete();
+
+ assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
+ assertEquals("{\"ok\":true}", response.getBodyAsString().block());
+ }
+
+ @Test
+ void testUpstreamStatusRetainedWhenGeneratedStatusIsOmitted() {
+ MockServerHttpResponse response = (MockServerHttpResponse)
exchange.getResponse();
+ response.setStatusCode(HttpStatus.ACCEPTED);
+ 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("Content-Type:
application/json\n\n{\"ok\":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 responseBody =
response.bufferFactory().wrap("original".getBytes(StandardCharsets.UTF_8));
+
+
StepVerifier.create(decorator.writeWith(Mono.just(responseBody))).verifyComplete();
+
+ assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
+ assertEquals("{\"ok\":true}", response.getBodyAsString().block());
+ }
+
+ @Test
+ void testNoContentStatusDropsBody() {
+ MockServerHttpResponse response = (MockServerHttpResponse)
exchange.getResponse();
+ response.setStatusCode(HttpStatus.OK);
+ 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 204 No Content\nContent-Type:
application/json\n\n"));
+ AiResponseTransformerPlugin.AiResponseTransformerDecorator decorator =
+ new
AiResponseTransformerPlugin.AiResponseTransformerDecorator(exchange, template,
chatClient);
+ DataBuffer responseBody =
response.bufferFactory().wrap("original".getBytes(StandardCharsets.UTF_8));
+
+
StepVerifier.create(decorator.writeWith(Mono.just(responseBody))).verifyComplete();
+
+ assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
+ assertNull(response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
+ assertEquals("", response.getBodyAsString().block());
+ }
+
@Test
void testOriginalHeadersRemainWhenTransformedBodyIsInvalid() {
MockServerHttpResponse response = (MockServerHttpResponse)
exchange.getResponse();
@@ -347,6 +455,45 @@ class AiResponseTransformerPluginTest {
assertNull(body);
}
+ @Test
+ void testExtractStatusFromAiResponse() {
+ assertEquals(HttpStatus.NOT_FOUND.value(),
+
AiResponseTransformerPlugin.extractStatusFromAiResponse("HTTP/1.1 404 Not
Found\nContent-Type: application/json\n\n{}"));
+ assertEquals(HttpStatus.NO_CONTENT.value(),
+
AiResponseTransformerPlugin.extractStatusFromAiResponse("HTTP/1.1 204 No
Content\n\n"));
+ assertEquals(HttpStatus.OK.value(),
+
AiResponseTransformerPlugin.extractStatusFromAiResponse("HTTP/1.0 200
OK\nContent-Type: application/json\n\n{}"));
+ }
+
+ @Test
+ void testExtractStatusFromAiResponseWithCodeBlock() {
+ assertEquals(HttpStatus.CREATED.value(), AiResponseTransformerPlugin
+ .extractStatusFromAiResponse("```\nHTTP/1.1 201
Created\nContent-Type: application/json\n\n{\"id\":1}\n```"));
+ }
+
+ @Test
+ void testExtractStatusFromAiResponseWithInvalidOrMissingStatusLine() {
+
assertNull(AiResponseTransformerPlugin.extractStatusFromAiResponse("HTTP/1.1
999 Not Real\nContent-Type: application/json\n\n{}"));
+
assertNull(AiResponseTransformerPlugin.extractStatusFromAiResponse("HTTP/1.1
abc OK\nContent-Type: application/json\n\n{}"));
+
assertNull(AiResponseTransformerPlugin.extractStatusFromAiResponse("Content-Type:
application/json\n\n{}"));
+
assertNull(AiResponseTransformerPlugin.extractStatusFromAiResponse("{\"status\":\"success\"}"));
+
assertNull(AiResponseTransformerPlugin.extractStatusFromAiResponse(""));
+
assertNull(AiResponseTransformerPlugin.extractStatusFromAiResponse(null));
+ }
+
+ @Test
+ void testExtractStatusFromAiResponseIgnoresStatusLookingHeaderValue() {
+ assertNull(AiResponseTransformerPlugin
+ .extractStatusFromAiResponse("Content-Type:
application/json\nX-Debug: HTTP/1.1 500 Internal Server Error\n\n{}"));
+ }
+
+ @Test
+ void testExtractHeadersFromAiResponseWithHttp10StatusLine() {
+ HttpHeaders headers = AiResponseTransformerPlugin
+ .extractHeadersFromAiResponse("HTTP/1.0 200 OK\nContent-Type:
application/json\n\n{\"status\":\"success\"}");
+ assertEquals("application/json", headers.getFirst("Content-Type"));
+ }
+
@Test
void testExtractHeadersFromAiResponseWithEmptyResponse() {
String aiResponse = "";