This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 21676fc85 fix(studio): return SSE refusal for missing AI message body
(#5088)
21676fc85 is described below
commit 21676fc852bcde58595d66c25508d08495264bc7
Author: coder999o <[email protected]>
AuthorDate: Thu Oct 1 17:28:18 2026 +0800
fix(studio): return SSE refusal for missing AI message body (#5088)
---
.../ops/ai/conversation/AiConversationController.java | 7 ++++++-
.../ops/ai/conversation/AiStreamControllerTest.java | 17 +++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationController.java
index b43d87f28..d31b2ec4a 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationController.java
@@ -132,6 +132,7 @@ public class AiConversationController {
/** Fallback for a constraint that carries no message; the wording {@code
GlobalExceptionHandler} uses. */
private static final String INVALID_REQUEST_MESSAGE = "Invalid request";
+ private static final String MESSAGE_REQUIRED_MESSAGE = "message is
required";
private final AiConversationService conversationService;
private final AiRunService runService;
@@ -236,12 +237,16 @@ public class AiConversationController {
*/
@PostMapping(value = "/conversations/{id}/messages", produces =
MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter sendMessage(@PathVariable("id") Long conversationId,
- @Valid @RequestBody AiMessageDTO request,
+ @Valid @RequestBody(required = false)
AiMessageDTO request,
BindingResult bindingResult,
HttpServletResponse response) {
// Both outcomes of this endpoint are event streams — an admitted run
and a refusal alike — so the
// headers go on first. A refusal is converted rather than thrown: see
refusalStream.
streamingHeaders(response);
+ if (request == null) {
+ return runService.refusalStream(HttpStatus.BAD_REQUEST.value(),
AiRunService.REFUSED_INVALID_CODE,
+ MESSAGE_REQUIRED_MESSAGE, null);
+ }
if (bindingResult.hasErrors()) {
// A BindingResult directly after the @Valid body makes Spring
hand the violations here
// instead of throwing MethodArgumentNotValidException. That is
the only way to answer them
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiStreamControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiStreamControllerTest.java
index c96a1e755..b46d81a9b 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiStreamControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiStreamControllerTest.java
@@ -276,6 +276,23 @@ class AiStreamControllerTest extends WebMvcAuthTestSupport
{
verify(runService).refusalStream(unconfigured);
}
+ @Test
+ void sendMessageShouldRejectAMissingBodyInsideTheStreamTest() throws
Exception {
+ when(runService.refusalStream(eq(400),
eq(AiRunService.REFUSED_INVALID_CODE),
+ eq("message is required"), isNull()))
+ .thenReturn(new
AiRunTestSupport.RecordingSseEmitter(CLI_TIMEOUT_MILLIS));
+
+ mockMvc.perform(post("/api/ai/conversations/{id}/messages",
CONVERSATION_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.TEXT_EVENT_STREAM))
+ .andExpect(status().isOk())
+
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM))
+ .andExpect(header().string(ACCEL_BUFFERING_HEADER, "no"));
+
+ verify(runService).refusalStream(400,
AiRunService.REFUSED_INVALID_CODE, "message is required", null);
+ verify(runService, never()).sendMessage(any(), any());
+ }
+
@Test
void sendMessageShouldRejectABlankMessageInsideTheStreamTest() throws
Exception {
// The endpoint takes a BindingResult next to the @Valid body, so a
rejected body is answered in