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";