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 809fdae7 fix(provider): handle empty cloud catalog responses (#2041)
809fdae7 is described below

commit 809fdae72ef297754001d0959c11a782edf38f7f
Author: xdz997 <[email protected]>
AuthorDate: Fri Aug 14 10:31:49 2026 +0800

    fix(provider): handle empty cloud catalog responses (#2041)
    
    Consolidates #1815 and #2041: return an empty trace instead of a 404 when
    Aliyun trace queries have no data, and prefer TotalCount when Tencent
    countTopics receives an empty data page.
---
 .../provider/alibaba/AliyunInstanceProvider.java   | 10 +++-
 .../provider/tencent/TencentInstanceProvider.java  | 16 +++---
 .../alibaba/AliyunInstanceProviderTest.java        | 44 ++++++++++++++++
 .../tencent/TencentInstanceProviderTest.java       | 58 ++++++++++++++++++++++
 4 files changed, 121 insertions(+), 7 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
index 56c38b37..6d0d0556 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
@@ -61,6 +61,7 @@ import org.springframework.stereotype.Component;
 import lombok.RequiredArgsConstructor;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -441,11 +442,18 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
         GetTraceResponseBody body = response == null ? null : 
response.getBody();
         GetTraceResponseBody.Data data = body == null ? null : body.getData();
         if (data == null) {
-            throw new BusinessException(404, "Message trace not found: " + 
msgId);
+            return emptyTraceRecord();
         }
         return AliyunConverters.toTraceRecord(data);
     }
 
+    private static TraceRecordVO emptyTraceRecord() {
+        return TraceRecordVO.builder()
+                .nodes(Collections.emptyList())
+                .consumerStatus(Collections.emptyList())
+                .build();
+    }
+
     private Context resolve(String instanceId) {
         if (!StringUtils.hasText(instanceId)) {
             throw new BusinessException(400, "instanceId is required");
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 6e38ce1a..a70c8fe4 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
@@ -132,18 +132,22 @@ public class TencentInstanceProvider implements 
InstanceProvider {
         request.setLimit(1L);
         DescribeTopicListResponse response = 
clientFactory.call(context.credentialId(), context.regionId(),
                 client -> client.DescribeTopicList(request));
-        if (response == null || response.getData() == null || 
response.getData().length == 0) {
+        if (response == null) {
             return 0;
         }
-        // Use the response total count if available; otherwise fall back to 
full scan
+        // TotalCount is independent of the current page contents and remains 
authoritative
+        // when Tencent returns an empty Data array for the minimal count 
request.
         Long total = response.getTotalCount();
-        if (total == null) {
-            return listTopics(instanceId, null, null, false).size();
+        if (total != null) {
+            if (total <= 0L) {
+                return 0;
+            }
+            return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : 
total.intValue();
         }
-        if (total <= 0L) {
+        if (response.getData() == null || response.getData().length == 0) {
             return 0;
         }
-        return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : 
total.intValue();
+        return listTopics(instanceId, null, null, false).size();
     }
 
     @Override
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 d39d0a6f..961a89c8 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
@@ -360,6 +360,50 @@ class AliyunInstanceProviderTest {
         assertThat(consumer.getStatus()).isEqualTo("CONSUME_OK");
     }
 
+    @Test
+    void getMessageTraceShouldReturnEmptyTraceWhenAliyunDataIsNullTest() {
+        stubInstance();
+        stubCallThrough();
+        GetTraceResponse response = GetTraceResponse.create().toBuilder()
+                .statusCode(200)
+                .body(GetTraceResponseBody.builder().data(null).build())
+                .build();
+        
when(asyncClient.getTrace(any())).thenReturn(CompletableFuture.completedFuture(response));
+
+        TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID, 
"msg-without-trace", "orders");
+
+        assertThat(trace.getNodes()).isEmpty();
+        assertThat(trace.getConsumerStatus()).isEmpty();
+    }
+
+    @Test
+    void getMessageTraceShouldReturnEmptyTraceWhenAliyunBodyIsNullTest() {
+        stubInstance();
+        stubCallThrough();
+        GetTraceResponse response = GetTraceResponse.create().toBuilder()
+                .statusCode(200)
+                .body(null)
+                .build();
+        
when(asyncClient.getTrace(any())).thenReturn(CompletableFuture.completedFuture(response));
+
+        TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID, 
"msg-without-body", "orders");
+
+        assertThat(trace.getNodes()).isEmpty();
+        assertThat(trace.getConsumerStatus()).isEmpty();
+    }
+
+    @Test
+    void getMessageTraceShouldReturnEmptyTraceWhenAliyunResponseIsNullTest() {
+        stubInstance();
+        stubCallThrough();
+        
when(asyncClient.getTrace(any())).thenReturn(CompletableFuture.completedFuture(null));
+
+        TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID, 
"msg-without-response", "orders");
+
+        assertThat(trace.getNodes()).isEmpty();
+        assertThat(trace.getConsumerStatus()).isEmpty();
+    }
+
     @Test
     void mappedBusinessExceptionShouldPropagateTest() {
         stubInstance();
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 f0459255..67b83c70 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
@@ -32,6 +32,7 @@ import 
com.tencentcloudapi.trocket.v20230308.models.MessageItem;
 import com.tencentcloudapi.trocket.v20230308.models.MessageTraceItem;
 import 
com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListByGroupResponse;
 import 
com.tencentcloudapi.trocket.v20230308.models.DescribeConsumerGroupListRequest;
+import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListRequest;
 import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListResponse;
 import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicRequest;
 import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicResponse;
@@ -121,6 +122,35 @@ class TencentInstanceProviderTest {
         
assertThat(provider.countTopics(STUDIO_INSTANCE_ID)).isEqualTo(Integer.MAX_VALUE);
     }
 
+    @Test
+    void countTopicsShouldUseTotalCountFromSingleItemRequestTest() throws 
Exception {
+        DescribeTopicListResponse response = new DescribeTopicListResponse();
+        response.setTotalCount(501L);
+        response.setData(new TopicItem[]{topicItem("orders", "NORMAL", 8L)});
+        when(client.DescribeTopicList(any())).thenReturn(response);
+
+        int count = provider.countTopics(STUDIO_INSTANCE_ID);
+
+        assertThat(count).isEqualTo(501);
+        ArgumentCaptor<DescribeTopicListRequest> captor =
+                ArgumentCaptor.forClass(DescribeTopicListRequest.class);
+        verify(client).DescribeTopicList(captor.capture());
+        
assertThat(captor.getValue().getInstanceId()).isEqualTo(CLOUD_INSTANCE_ID);
+        assertThat(captor.getValue().getOffset()).isZero();
+        assertThat(captor.getValue().getLimit()).isEqualTo(1L);
+    }
+
+    @Test
+    void countTopicsShouldUseTotalCountEvenWhenDataIsEmptyTest() throws 
Exception {
+        DescribeTopicListResponse response = new DescribeTopicListResponse();
+        response.setTotalCount(501L);
+        response.setData(new TopicItem[0]);
+        when(client.DescribeTopicList(any())).thenReturn(response);
+
+        assertThat(provider.countTopics(STUDIO_INSTANCE_ID)).isEqualTo(501);
+        verify(client, times(1)).DescribeTopicList(any());
+    }
+
     @Test
     void listTopicsShouldMapAndFilterAndEnrichTimesTest() throws Exception {
         TopicItem normal = topicItem("orders", "NORMAL", 8L);
@@ -355,6 +385,34 @@ class TencentInstanceProviderTest {
                 .containsOnly(100L);
     }
 
+    @Test
+    void 
getTopicConsumersShouldStopAfterSubscriptionCountReachedOnFullPageTest() throws 
Exception {
+        DescribeTopicResponse firstPage = new DescribeTopicResponse();
+        firstPage.setSubscriptionCount(200L);
+        firstPage.setSubscriptionData(IntStream.range(0, 100)
+                .mapToObj(index -> subscription("GID_" + index))
+                .toArray(SubscriptionData[]::new));
+        DescribeTopicResponse secondPage = new DescribeTopicResponse();
+        secondPage.setSubscriptionCount(200L);
+        secondPage.setSubscriptionData(IntStream.range(100, 200)
+                .mapToObj(index -> subscription("GID_" + index))
+                .toArray(SubscriptionData[]::new));
+        when(client.DescribeTopic(any())).thenReturn(firstPage, secondPage);
+
+        List<TopicConsumerVO> consumers = 
provider.getTopicConsumers(STUDIO_INSTANCE_ID, "orders");
+
+        assertThat(consumers).hasSize(200);
+        assertThat(consumers.get(199).getGroup()).isEqualTo("GID_199");
+        ArgumentCaptor<DescribeTopicRequest> captor = 
ArgumentCaptor.forClass(DescribeTopicRequest.class);
+        verify(client, times(2)).DescribeTopic(captor.capture());
+        assertThat(captor.getAllValues())
+                .extracting(DescribeTopicRequest::getOffset)
+                .containsExactly(0L, 100L);
+        assertThat(captor.getAllValues())
+                .extracting(DescribeTopicRequest::getLimit)
+                .containsOnly(100L);
+    }
+
     @Test
     void listConsumerGroupsShouldClampOversizedRetryCounts() throws Exception {
         ConsumeGroupItem item = new ConsumeGroupItem();

Reply via email to