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 f5466861c fix(dlq): preserve binary message bodies in exports (#5205)
f5466861c is described below

commit f5466861cbcbd5801231af1988f6de3d3aaad5d2
Author: coder999o <[email protected]>
AuthorDate: Fri Oct 9 11:41:45 2026 +0800

    fix(dlq): preserve binary message bodies in exports (#5205)
    
    The DLQ export decoded every message body as UTF-8, so a binary body 
reached the xlsx as replacement
    characters and the original bytes were lost; the JSON payload had the same 
problem. Decoding is now
    strict (`onMalformedInput` / `onUnmappableCharacter` = REPORT): when the 
bytes are not valid UTF-8,
    `body` is null and `bodyBase64` carries the raw bytes, and the Excel sheet 
gains a `Body Base64`
    column that is populated only in that case. `docs/api-spec.md` is updated 
for both the message
    payload and the export columns.
    
    Maintainer edits on top of the contribution, both inside the new test: the 
`searchOffset` stub now
    returns `0L, 1L` like the neighbouring cases, because resolving the offset 
window has taken two
    calls since #5104 (a single value made the case throw 
`IndexOutOfBoundsException`), and the method
    was renamed to end with `Test` per the repository convention.
---
 docs/api-spec.md                                    |  4 ++--
 .../studio/instance/dlq/DLQMessageExcelRow.java     |  3 +++
 .../studio/provider/apache/RocketMQDLQProvider.java | 11 +++++++++--
 .../studio/instance/dlq/DLQMessageExcelRowTest.java | 14 ++++++++++++++
 .../provider/apache/RocketMQDLQProviderTest.java    | 21 +++++++++++++++++++++
 5 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/docs/api-spec.md b/docs/api-spec.md
index 24ba69c4c..2205e6233 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1752,7 +1752,7 @@ GET 
/api/dlq/{groupName}/messages?instanceId={instanceId}&startTime={ms}&endTime
 | `storeTime` | `number` | Broker 存储时间(Unix 毫秒时间戳) |
 | `reconsumeTimes` | `number` | 已重试次数 |
 | `keys` | `string` | 消息 Key,可能为 `null` |
-| `body` | `string` | 按 UTF-8 解码的消息体;消息没有 body 时为 `null` |
+| `body` | `string` | 严格按 UTF-8 解码的消息体;原始字节不是有效 UTF-8 或消息没有 body 时为 `null` |
 | `bodyBase64` | `string` | 原始字节的 Base64,供二进制消息无损导出;消息没有 body 时为 `null` |
 | `properties` | `Record<string, string>` | 用户属性。已剔除 Broker 系统属性,按 key 排序,最多 
`64` 条,单值超过 `1024` 码点时截断并追加 `...` |
 | `propertiesTruncated` | `boolean` | 属性条数或单值长度是否触发了上述截断 |
@@ -1814,7 +1814,7 @@ GET 
/api/dlq/export-excel?instanceId={instanceId}&groupName={groupName}&startTim
 | `endTime` | `number` | 否 | 结束时间(Unix 毫秒时间戳) |
 | `msgIds` | `string[]` | 否 | 只导出选中的消息,最多 `100` 条;省略则导出整个时间窗口 |
 
-**Response:** 不走统一 `Result` 外壳,直接返回 `.xlsx` 字节流,`Content-Type: 
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,`Content-Disposition:
 attachment; filename="dlq-{groupName}.xlsx"`。工作表名 `DLQ`,列为 `Message ID` / 
`Topic` / `Queue ID` / `Offset` / `Store Time` / `Reconsume Times` / `Keys` / 
`Body`,其中 `Store Time` 按服务端默认时区格式化为 `yyyy-MM-dd HH:mm:ss`。生成失败返回 `502 Failed to 
export DLQ messages as Excel`。
+**Response:** 不走统一 `Result` 外壳,直接返回 `.xlsx` 字节流,`Content-Type: 
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,`Content-Disposition:
 attachment; filename="dlq-{groupName}.xlsx"`。工作表名 `DLQ`,列为 `Message ID` / 
`Topic` / `Queue ID` / `Offset` / `Store Time` / `Reconsume Times` / `Keys` / 
`Body` / `Body Base64`;原始字节不是有效 UTF-8 时 `Body` 为空,`Body Base64` 
保存原始字节,其他情况下该列为空。`Store Time` 按服务端默认时区格式化为 `yyyy-MM-dd HH:mm:ss`。生成失败返回 `502 
Failed to export DLQ messages as Excel`。
 
 `msgIds` 使用重复同名参数(`msgIds=a&msgIds=b`)。省略 `msgIds` 
才会导出整个时间窗口;显式传入空值(`msgIds=`)会返回 `400`。方括号或点号索引形式(如 `msgIds[]=a`、`msgIds[0]=a` 或 
`msgIds.0=a`)也会返回 `400`,避免误将选中消息导出变成整个窗口导出。
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRow.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRow.java
index 6d06c2542..121f47a41 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRow.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRow.java
@@ -49,6 +49,8 @@ public class DLQMessageExcelRow {
     private String keys;
     @ExcelProperty("Body")
     private String body;
+    @ExcelProperty("Body Base64")
+    private String bodyBase64;
 
     public static DLQMessageExcelRow from(DLQMessageVO vo) {
         DLQMessageExcelRow row = new DLQMessageExcelRow();
@@ -61,6 +63,7 @@ public class DLQMessageExcelRow {
         row.setReconsumeTimes(vo.getReconsumeTimes());
         row.setKeys(vo.getKeys());
         row.setBody(vo.getBody());
+        row.setBodyBase64(vo.getBody() == null ? vo.getBodyBase64() : null);
         return row;
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
index 107ebf8e3..0578f23c3 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
@@ -56,6 +56,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.time.LocalDateTime;
@@ -415,8 +418,12 @@ public class RocketMQDLQProvider implements DLQProvider {
             return null;
         }
         try {
-            return new String(body, StandardCharsets.UTF_8);
-        } catch (Exception ignored) {
+            return StandardCharsets.UTF_8.newDecoder()
+                    .onMalformedInput(CodingErrorAction.REPORT)
+                    .onUnmappableCharacter(CodingErrorAction.REPORT)
+                    .decode(ByteBuffer.wrap(body))
+                    .toString();
+        } catch (CharacterCodingException ignored) {
             return null;
         }
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRowTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRowTest.java
index cbb5d866f..c821ff7f1 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRowTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageExcelRowTest.java
@@ -49,6 +49,20 @@ class DLQMessageExcelRowTest {
                 .containsEntry("Reconsume Times", "3");
     }
 
+    @Test
+    void shouldPreserveBinaryBodyInBase64ColumnTest() {
+        DLQMessageVO message = DLQMessageVO.builder()
+                .msgId("binary-msg")
+                .storeTime(1_700_000_000_000L)
+                .bodyBase64("wyg=")
+                .build();
+
+        Map<String, String> cells = firstDataRowByColumnName(List.of(message));
+
+        assertThat(cells.get("Body")).isNull();
+        assertThat(cells).containsEntry("Body Base64", "wyg=");
+    }
+
     private static Map<String, String> 
firstDataRowByColumnName(List<DLQMessageVO> messages) {
         List<Map<Integer, String>> rows = readBack(messages);
         Map<Integer, String> header = rows.get(0);
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index 5d85c9abf..8596179c9 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -1006,6 +1006,27 @@ class RocketMQDLQProviderTest {
                 .isEqualTo(Base64.getEncoder().encodeToString("hello 
dlq".getBytes(StandardCharsets.UTF_8)));
     }
 
+    @Test
+    void exportMessagesDoesNotPresentInvalidUtf8AsTextTest() throws Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        byte[] body = {(byte) 0xC3, (byte) 0x28};
+        MessageExt deadLetter = new MessageExt();
+        deadLetter.setMsgId("binary-msg");
+        deadLetter.setStoreTimestamp(150L);
+        deadLetter.setBody(body);
+        
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+        when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L, 
1L);
+        when(pullConsumer.pull(eq(queue), eq("*"), eq(0L), eq(32)))
+                .thenReturn(new PullResult(PullStatus.FOUND, 1L, 0L, 0L, 
List.of(deadLetter)));
+
+        DLQMessageVO exported = provider.exportMessages("instance-a", 
"group-a", 100L, 200L, 1000)
+                .getMessages().get(0);
+
+        assertThat(exported.getBody()).isNull();
+        
assertThat(exported.getBodyBase64()).isEqualTo(Base64.getEncoder().encodeToString(body));
+    }
+
     @Test
     void exportMessagesHonorsMaxCountCap() throws Exception {
         String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";

Reply via email to