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 ec912d32 fix(server): correct Aliyun and Tencent OpenAPI parsing 
(#1978)
ec912d32 is described below

commit ec912d32f7563bc9934358bf1bdda862e9b6bcec
Author: 0 <[email protected]>
AuthorDate: Thu Aug 13 16:12:07 2026 +0800

    fix(server): correct Aliyun and Tencent OpenAPI parsing (#1978)
    
    * fix(web): migrate deprecated Ant Design props across Studio pages
    
    Consolidates 17 per-page migration PRs by 123123213weqw: message (#1835),
    certificate (#1836), client (#1837), cluster (#1838), ACL (#1839),
    consumer (#1840), DLQ (#1841), instance (#1842), topic (#1843),
    alert rule (#1844), settings modal (#1845), alert asset modal (#1846),
    group management (#1847), LiteTopic (#1848), producer (#1849),
    proxy (#1850), SSL settings (#1851). Behavior unchanged.
    
    * fix(server): harden validation, normalization and null-safety
    
    Consolidates 40 backend robustness PRs by 123123213weqw (#1114,
    #1733-#1735, #1737-#1739, #1742, #1744-#1745, #1854-#1882, #1884):
    block new admin connections during shutdown, Locale.ROOT normalization
    for LLM engines and agent providers, reject null or malformed persisted
    JSON, clamp cloud catalog counts and retry values, skip null catalog,
    trace and connection entries, report lost concurrent updates for alert
    rules, ACL rules, credentials, instances and certificates, normalize
    stored enum values, validate alert identifiers and stored auth modes.
    
    * fix(web): guard stale responses and race conditions in Studio pages
    
    Consolidates 16 frontend UX PRs by 123123213weqw (#1563, #1624, #1732,
    #1743, #1747-#1750, #1852-#1853, #1883, #2020-#2024): DLQ resend request-id
    guard, best-effort proxy address persistence, locale-independent navigation
    search, preserve selected home prompt mode, normalize alert levels before
    filtering, restore audit refresh loading state, bound AI SSE event buffers,
    format blank dates as unavailable, hook mocks for instance changes and
    topic-scoped traces, keep 401 errors for malformed request URLs, prevent
    Mock-mode home reload loop, surface Message Explorer topic load failures,
    refresh alerts after clearing acknowledged records, gate data source
    mutations until the initial list is ready, ignore stale NameServer drift
    bootstrap results.
    
    * fix(server): correct Aliyun and Tencent OpenAPI parsing
    
    Consolidates 3 cloud-vendor PRs by 123123213weqw (#1978-#1980): map
    in-flight Tencent consume trace nodes to process instead of failed, parse
    Aliyun OpenAPI timestamps as UTC+8 regardless of server zone, reuse the
    TaskRequestId returned by the previous Tencent message query page.
---
 .../studio/provider/alibaba/AliyunConverters.java  | 39 ++++++++-
 .../provider/tencent/TencentInstanceProvider.java  | 37 +++++++--
 .../alibaba/AliyunInstanceProviderTest.java        | 11 +++
 .../tencent/TencentInstanceProviderTest.java       | 95 ++++++++++++++++++++++
 4 files changed, 173 insertions(+), 9 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
index 8e501d0b..597b65d5 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
@@ -66,6 +66,9 @@ final class AliyunConverters {
     static final int MESSAGE_MAX_PAGES = 5;
 
     private static final DateTimeFormatter TIME_FORMATTER = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    // Aliyun RocketMQ OpenAPI timestamps are unzoned "yyyy-MM-dd HH:mm:ss" 
strings interpreted as
+    // UTC+8 (Asia/Shanghai), regardless of the server's default zone.
+    private static final ZoneId ALIYUN_TIME_ZONE = ZoneId.of("Asia/Shanghai");
 
     private AliyunConverters() {
     }
@@ -96,6 +99,9 @@ final class AliyunConverters {
         List<CloudInstanceDetailVO.CloudEndpoint> endpoints = new 
ArrayList<>();
         if (data.getNetworkInfo() != null && 
data.getNetworkInfo().getEndpoints() != null) {
             for (GetInstanceResponseBody.Endpoints endpoint : 
data.getNetworkInfo().getEndpoints()) {
+                if (endpoint == null) {
+                    continue;
+                }
                 endpoints.add(new CloudInstanceDetailVO.CloudEndpoint(
                         endpoint.getEndpointType(), 
endpoint.getEndpointUrl()));
             }
@@ -211,7 +217,7 @@ final class AliyunConverters {
                 .msgId(data.getMessageId())
                 .topic(data.getTopicName())
                 .tag(data.getMessageTag())
-                .key(data.getMessageKeys() == null ? null : String.join(" ", 
data.getMessageKeys()))
+                .key(joinMessageKeys(data.getMessageKeys()))
                 .bornHost(data.getBornHost())
                 .storeHost(data.getStoreHost())
                 .storeTime(parseTimeMillis(data.getStoreTime()))
@@ -229,6 +235,9 @@ final class AliyunConverters {
         List<TraceNodeVO> nodes = new ArrayList<>();
         if (data.getProducerInfo() != null && 
data.getProducerInfo().getRecords() != null) {
             for (GetTraceResponseBody.ProducerInfoRecords record : 
data.getProducerInfo().getRecords()) {
+                if (record == null) {
+                    continue;
+                }
                 nodes.add(TraceNodeVO.builder()
                         .title("Producer")
                         .timestamp(parseTimeMillis(record.getProduceTime()))
@@ -240,6 +249,9 @@ final class AliyunConverters {
         }
         if (data.getBrokerInfo() != null && 
data.getBrokerInfo().getOperations() != null) {
             for (GetTraceResponseBody.Operations operation : 
data.getBrokerInfo().getOperations()) {
+                if (operation == null) {
+                    continue;
+                }
                 nodes.add(TraceNodeVO.builder()
                         .title("Broker " + operation.getOperateType())
                         .timestamp(parseTimeMillis(operation.getOperateTime()))
@@ -248,6 +260,9 @@ final class AliyunConverters {
         }
         if (data.getConsumerInfos() != null) {
             for (GetTraceResponseBody.ConsumerInfos consumerInfo : 
data.getConsumerInfos()) {
+                if (consumerInfo == null) {
+                    continue;
+                }
                 if (consumerInfo.getRecords() == null || 
consumerInfo.getRecords().isEmpty()) {
                     nodes.add(TraceNodeVO.builder()
                             .title("Consumer " + 
consumerInfo.getConsumerGroupId())
@@ -256,6 +271,9 @@ final class AliyunConverters {
                     continue;
                 }
                 for (GetTraceResponseBody.Records record : 
consumerInfo.getRecords()) {
+                    if (record == null) {
+                        continue;
+                    }
                     String operateTime = null;
                     if (record.getOperations() != null && 
!record.getOperations().isEmpty()) {
                         operateTime = 
record.getOperations().get(0).getOperateTime();
@@ -289,7 +307,7 @@ final class AliyunConverters {
         }
         try {
             LocalDateTime dateTime = LocalDateTime.parse(value, 
TIME_FORMATTER);
-            return 
dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+            return 
dateTime.atZone(ALIYUN_TIME_ZONE).toInstant().toEpochMilli();
         } catch (RuntimeException ignored) {
             return 0L;
         }
@@ -297,7 +315,7 @@ final class AliyunConverters {
 
     static String formatTimeMillis(long epochMillis) {
         return TIME_FORMATTER.format(
-                LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), 
ZoneId.systemDefault()));
+                LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), 
ALIYUN_TIME_ZONE));
     }
 
     static String tryBase64Decode(String raw) {
@@ -321,6 +339,10 @@ final class AliyunConverters {
         }
     }
 
+    private static String joinMessageKeys(List<String> keys) {
+        return keys == null ? null : joinParts(" ", 
keys.toArray(String[]::new));
+    }
+
     private static String joinParts(String separator, String... parts) {
         StringBuilder sb = new StringBuilder();
         for (String part : parts) {
@@ -336,6 +358,15 @@ final class AliyunConverters {
     }
 
     private static Integer toInteger(Long value) {
-        return value == null ? null : value.intValue();
+        if (value == null) {
+            return null;
+        }
+        if (value > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+        if (value < 0) {
+            return 0;
+        }
+        return value.intValue();
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
index f205d5bc..f249d92d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
@@ -137,7 +137,13 @@ public class TencentInstanceProvider implements 
InstanceProvider {
         }
         // Use the response total count if available; otherwise fall back to 
full scan
         Long total = response.getTotalCount();
-        return total != null ? Math.toIntExact(total) : listTopics(instanceId, 
null, null, false).size();
+        if (total == null) {
+            return listTopics(instanceId, null, null, false).size();
+        }
+        if (total <= 0L) {
+            return 0;
+        }
+        return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : 
total.intValue();
     }
 
     @Override
@@ -476,6 +482,8 @@ public class TencentInstanceProvider implements 
InstanceProvider {
             request.setTopic(topic);
             request.setStartTime(begin);
             request.setEndTime(end);
+            // Reuse the task id returned by the previous page so paging 
continues the same
+            // logical query; fall back to the initial random id when the API 
omits it.
             request.setTaskRequestId(taskRequestId);
             if (StringUtils.hasText(key)) {
                 request.setMsgKey(key);
@@ -489,6 +497,9 @@ public class TencentInstanceProvider implements 
InstanceProvider {
                     client -> client.DescribeMessageList(request));
             MessageItem[] data = response == null ? null : response.getData();
             long total = response == null ? 0L : (response.getTotalCount() == 
null ? 0L : response.getTotalCount());
+            if (response != null && 
StringUtils.hasText(response.getTaskRequestId())) {
+                taskRequestId = response.getTaskRequestId();
+            }
             if (data != null) {
                 for (MessageItem item : data) {
                     if (item != null) {
@@ -673,7 +684,12 @@ public class TencentInstanceProvider implements 
InstanceProvider {
                 return Collections.emptyMap();
             }
             Map<String, String> properties = new LinkedHashMap<>();
-            root.fields().forEachRemaining(entry -> 
properties.put(entry.getKey(), entry.getValue().asText("")));
+            root.fields().forEachRemaining(entry -> {
+                JsonNode value = entry.getValue();
+                if (value != null && value.isValueNode() && !value.isNull()) {
+                    properties.put(entry.getKey(), value.asText());
+                }
+            });
             return properties;
         } catch (Exception e) {
             return Collections.emptyMap();
@@ -715,8 +731,16 @@ public class TencentInstanceProvider implements 
InstanceProvider {
     }
 
     private static String toConsumeTraceStatus(int status) {
-        // Tencent consume log Status uses the RocketMQ convention where 2 
means consumed.
-        return status == 2 ? "finish" : "failed";
+        // Tencent consume log Status uses the RocketMQ convention where 0/1 
are in-flight and
+        // 2 means consumed; keep the trace status consistent with 
toDeliveryStatus and with the
+        // frontend TraceNode.status values ('error' | 'wait' | 'process' | 
'finish').
+        if (status == 2) {
+            return "finish";
+        }
+        if (status == 0 || status == 1) {
+            return "process";
+        }
+        return "error";
     }
 
     private static DeliveryStatus toDeliveryStatus(int status) {
@@ -859,7 +883,10 @@ public class TencentInstanceProvider implements 
InstanceProvider {
     }
 
     private static int toInt(Long value) {
-        return value == null ? 0 : Math.toIntExact(value);
+        if (value == null || value <= 0L) {
+            return 0;
+        }
+        return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : 
value.intValue();
     }
 
     private static ConsumeType toConsumeType(Boolean consumeMessageOrderly) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
index a3d461f4..d39d0a6f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
@@ -555,4 +555,15 @@ class AliyunInstanceProviderTest {
         org.junit.jupiter.api.Assertions.assertEquals("Concurrently",
                 
AliyunInstanceProvider.normalizeDeliveryOrderType("Concurrently"));
     }
+
+    @Test
+    void timeConversionUsesAliyunUtc8Zone() {
+        // "2024-01-01 00:00:00" is 2023-12-31T16:00:00Z in UTC+8 regardless 
of server zone.
+        long expectedUtc8 = java.time.LocalDateTime.of(2024, 1, 1, 0, 0)
+                
.atZone(java.time.ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli();
+        assertThat(AliyunConverters.parseTimeMillis("2024-01-01 
00:00:00")).isEqualTo(expectedUtc8);
+
+        // Round-trip formatting must restore the same calendar time in UTC+8.
+        
assertThat(AliyunConverters.formatTimeMillis(expectedUtc8)).isEqualTo("2024-01-01
 00:00:00");
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
index 5fed4b7d..f61d5d0e 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
@@ -110,6 +110,16 @@ class TencentInstanceProviderTest {
         });
     }
 
+    @Test
+    void countTopicsShouldClampOversizedTotals() throws Exception {
+        DescribeTopicListResponse response = new DescribeTopicListResponse();
+        response.setData(new TopicItem[]{topicItem("orders", "NORMAL", 8L)});
+        response.setTotalCount(Long.MAX_VALUE);
+        when(client.DescribeTopicList(any())).thenReturn(response);
+
+        
assertThat(provider.countTopics(STUDIO_INSTANCE_ID)).isEqualTo(Integer.MAX_VALUE);
+    }
+
     @Test
     void listTopicsShouldMapAndFilterAndEnrichTimesTest() throws Exception {
         TopicItem normal = topicItem("orders", "NORMAL", 8L);
@@ -293,6 +303,20 @@ class TencentInstanceProviderTest {
                 .containsOnly(100L);
     }
 
+    @Test
+    void listConsumerGroupsShouldClampOversizedRetryCounts() throws Exception {
+        ConsumeGroupItem item = new ConsumeGroupItem();
+        item.setConsumerGroup("GID_test");
+        item.setMaxRetryTimes(Long.MAX_VALUE);
+        DescribeConsumerGroupListResponse response = new 
DescribeConsumerGroupListResponse();
+        response.setData(new ConsumeGroupItem[]{item});
+        when(client.DescribeConsumerGroupList(any())).thenReturn(response);
+
+        assertThat(provider.listConsumerGroups(STUDIO_INSTANCE_ID, null))
+                .singleElement()
+                .satisfies(group -> 
assertThat(group.getRetryMaxTimes()).isEqualTo(Integer.MAX_VALUE));
+    }
+
     @Test
     void listConsumerGroupsShouldMapAndFilterTest() throws Exception {
         ConsumeGroupItem one = new ConsumeGroupItem();
@@ -436,6 +460,25 @@ class TencentInstanceProviderTest {
         assertThat(captor.getValue().getMsgId()).isEqualTo("MSG-1");
     }
 
+    @Test
+    void queryMessagesShouldSkipNullAndStructuredPropertyValues() throws 
Exception {
+        DescribeMessageResponse detail = new DescribeMessageResponse();
+        detail.setMessageId("MSG-1");
+        detail.setShowTopicName("orders");
+        detail.setProperties("{\"KEYS\":\"keyA\",\"TAGS\":null,"
+                + "\"nested\":{\"x\":1},\"retry\":3,\"enabled\":true}");
+        when(client.DescribeMessage(any())).thenReturn(detail);
+
+        MessageRecordVO record = provider.queryMessages(
+                STUDIO_INSTANCE_ID, "orders", "MSG-1", null, null, null, 
null).get(0);
+
+        assertThat(record.getProperties())
+                .containsEntry("KEYS", "keyA")
+                .containsEntry("retry", "3")
+                .containsEntry("enabled", "true")
+                .doesNotContainKeys("TAGS", "nested");
+    }
+
     @Test
     void queryMessagesByTopicShouldUseMessageListTest() throws Exception {
         MessageItem one = new MessageItem();
@@ -511,6 +554,38 @@ class TencentInstanceProviderTest {
                 .isNotBlank();
     }
 
+    @Test
+    void messageQueryReusesTaskRequestIdReturnedByPreviousPage() throws 
Exception {
+        MessageItem[] page1Items = new 
MessageItem[TencentInstanceProvider.MESSAGE_LIMIT];
+        for (int i = 0; i < TencentInstanceProvider.MESSAGE_LIMIT; i++) {
+            MessageItem item = new MessageItem();
+            item.setMsgId("MSG-" + (i + 1));
+            item.setProduceTime("2024-09-12 14:06:55,591");
+            page1Items[i] = item;
+        }
+        MessageItem last = new MessageItem();
+        last.setMsgId("MSG-LAST");
+        last.setProduceTime("2024-09-12 14:06:56,591");
+        DescribeMessageListResponse page1 = new DescribeMessageListResponse();
+        page1.setData(page1Items);
+        page1.setTaskRequestId("task-abc");
+        DescribeMessageListResponse page2 = new DescribeMessageListResponse();
+        page2.setData(new MessageItem[]{last});
+        page2.setTaskRequestId("task-abc");
+        when(client.DescribeMessageList(any()))
+                .thenReturn(page1)
+                .thenReturn(page2);
+
+        provider.queryMessages(STUDIO_INSTANCE_ID, "orders", null, null, null,
+                1600000000000L, 1600001000000L);
+
+        ArgumentCaptor<DescribeMessageListRequest> captor = 
ArgumentCaptor.forClass(DescribeMessageListRequest.class);
+        verify(client, 
org.mockito.Mockito.times(2)).DescribeMessageList(captor.capture());
+        java.util.List<DescribeMessageListRequest> requests = 
captor.getAllValues();
+        // The second page must carry the task id returned by the first page, 
not a fresh random id.
+        assertThat(requests.get(1).getTaskRequestId()).isEqualTo("task-abc");
+    }
+
     @Test
     void getMessageTraceShouldMapStagesTest() throws Exception {
         MessageTraceItem produce = new MessageTraceItem();
@@ -546,4 +621,24 @@ class TencentInstanceProviderTest {
         assertThat(captor.getValue().getTopic()).isEqualTo("orders");
         assertThat(captor.getValue().getMsgId()).isEqualTo("MSG-1");
     }
+
+    @Test
+    void getMessageTraceMarksInFlightConsumeAsProcessNotFailed() throws 
Exception {
+        MessageTraceItem consume = new MessageTraceItem();
+        consume.setStage("consume");
+        
consume.setData("{\"TotalCount\":1,\"RocketMqConsumeLogs\":[{\"MsgId\":\"MSG-2\",\"Status\":1,"
+                + "\"PushTime\":\"2024-09-12 
14:06:55,600\",\"ConsumerGroup\":\"GID_test\",\"RetryTimes\":0}]}");
+        DescribeMessageTraceResponse response = new 
DescribeMessageTraceResponse();
+        response.setData(new MessageTraceItem[]{consume});
+        when(client.DescribeMessageTrace(any())).thenReturn(response);
+
+        TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID, 
"MSG-2", "orders");
+
+        assertThat(trace.getNodes()).hasSize(1);
+        // In-flight (Status 1) must read "process", consistent with 
toDeliveryStatus and the
+        // frontend TraceNode status union, not "failed".
+        assertThat(trace.getNodes().get(0).getStatus()).isEqualTo("process");
+        assertThat(trace.getConsumerStatus().get(0).getDeliveryStatus())
+                .isEqualTo(DeliveryStatus.pending);
+    }
 }

Reply via email to