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 1d4bbaf8e fix(message): treat MQClientException NO_MESSAGE key queries
as empty results (#3302)
1d4bbaf8e is described below
commit 1d4bbaf8e35a251101eb14f850ca9d254b6c534f
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Sep 7 17:28:54 2026 +0800
fix(message): treat MQClientException NO_MESSAGE key queries as empty
results (#3302)
MQAdminImpl.queryMessage throws MQClientException(ResponseCode.NO_MESSAGE,
"query message by key finished, but no message.") instead of returning an
empty QueryResult when the key matches nothing. queryByKey surfaced that as
a 502 gateway error, so searching a message key that simply has no matches -
a normal outcome - failed. The trace lookups hit the same client path:
getMessageTrace returned 502 for any message without trace data (trace
disabled on the producer or expired records), contradicting the provider's
own exception-grading convention that only grades TOPIC_NOT_EXIST.
Grade NO_MESSAGE alongside TOPIC_NOT_EXIST via a shared response-code check:
key queries return an empty list and both trace paths return an empty trace,
while genuine broker failures still surface as 502. This matches the
expected behavior recorded in issues #1161/#1275: a completed query with no
matching records is an empty result, not an error.
Co-authored-by: unbridled-41
<[email protected]>
---
.../studio/common/util/MqResponseCodes.java | 64 ++++++++++++++++++++
.../provider/apache/RocketMQMessageProvider.java | 36 +++++------
.../studio/common/util/MqResponseCodesTest.java | 69 ++++++++++++++++++++++
.../apache/RocketMQMessageProviderTest.java | 40 +++++++++++++
4 files changed, 191 insertions(+), 18 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/common/util/MqResponseCodes.java
b/server/src/main/java/org/apache/rocketmq/studio/common/util/MqResponseCodes.java
new file mode 100644
index 000000000..7270182be
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/common/util/MqResponseCodes.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.apache.rocketmq.client.exception.MQBrokerException;
+import org.apache.rocketmq.client.exception.MQClientException;
+
+/**
+ * Shared classifier for the RocketMQ response codes carried on broker/client
exceptions.
+ * Consolidates the per-provider copies that walk an exception cause chain
looking for a
+ * specific {@code ResponseCode} so the "RPC succeeded but there is no
business data" grading
+ * stays consistent. {@link MQClientException} and {@link MQBrokerException}
both expose
+ * {@code getResponseCode()} but share no common supertype, so both are
checked.
+ */
+public final class MqResponseCodes {
+
+ private MqResponseCodes() {
+ }
+
+ /**
+ * Returns {@code true} when any exception in the cause chain is an {@link
MQClientException}
+ * or {@link MQBrokerException} carrying one of the given response codes.
A self-referential
+ * cause terminates the walk instead of looping forever.
+ */
+ public static boolean hasResponseCode(Throwable error, int...
responseCodes) {
+ Throwable cause = error;
+ while (cause != null) {
+ Integer code = responseCodeOf(cause);
+ if (code != null) {
+ for (int responseCode : responseCodes) {
+ if (code == responseCode) {
+ return true;
+ }
+ }
+ }
+ cause = cause.getCause() == cause ? null : cause.getCause();
+ }
+ return false;
+ }
+
+ private static Integer responseCodeOf(Throwable throwable) {
+ if (throwable instanceof MQClientException clientException) {
+ return clientException.getResponseCode();
+ }
+ if (throwable instanceof MQBrokerException brokerException) {
+ return brokerException.getResponseCode();
+ }
+ return null;
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
index 2191c6ea4..f133dd7ce 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
@@ -17,7 +17,6 @@
package org.apache.rocketmq.studio.provider.apache;
import org.apache.rocketmq.client.QueryResult;
-import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.consumer.PullStatus;
@@ -31,6 +30,7 @@ import org.apache.rocketmq.remoting.protocol.route.QueueData;
import org.apache.rocketmq.remoting.protocol.route.TopicRouteData;
import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.common.util.MqResponseCodes;
import org.apache.rocketmq.studio.common.domain.enums.DeliveryStatus;
import org.apache.rocketmq.studio.instance.message.ConsumerStatusVO;
import org.apache.rocketmq.studio.instance.message.MessageProvider;
@@ -189,6 +189,13 @@ public class RocketMQMessageProvider implements
MessageProvider {
}
return result;
} catch (Exception e) {
+ if (MqResponseCodes.hasResponseCode(e, ResponseCode.NO_MESSAGE)) {
+ // MQAdminImpl.queryMessage throws
MQClientException(NO_MESSAGE) instead of
+ // returning an empty QueryResult when the key matches
nothing: the query
+ // completed, so the correct response is an empty list, not a
gateway error.
+ log.info("queryMessage(topic={}, key={}) matched nothing",
topic, key);
+ return Collections.emptyList();
+ }
log.warn("queryMessage(topic={}, key={}) failed: {}", topic, key,
e.getMessage());
throw new BusinessException(502, "Failed to query messages by key:
" + e.getMessage());
}
@@ -438,11 +445,12 @@ public class RocketMQMessageProvider implements
MessageProvider {
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
- if (isTraceTopicAbsent(e)) {
- // The cluster has no trace topic route (trace dispatch
disabled): the RPC
- // succeeded but there is no business data, so return an empty
trace instead
- // of surfacing an error (exception-grading convention).
- log.info("Trace topic not available on this cluster
(msgId={}), returning empty trace", msgId);
+ if (MqResponseCodes.hasResponseCode(e,
ResponseCode.TOPIC_NOT_EXIST, ResponseCode.NO_MESSAGE)) {
+ // The cluster has no trace topic route (trace dispatch
disabled) or the
+ // message simply has no trace records: the RPC succeeded but
there is no
+ // business data, so return an empty trace instead of
surfacing an error
+ // (exception-grading convention).
+ log.info("No trace data available for msgId={} ({}), returning
empty trace", msgId, e.getMessage());
return emptyTrace();
}
log.warn("Trace query for msgId={} failed: {}", msgId,
e.getMessage());
@@ -455,18 +463,6 @@ public class RocketMQMessageProvider implements
MessageProvider {
.build();
}
- private static boolean isTraceTopicAbsent(Throwable error) {
- Throwable cause = error;
- while (cause != null) {
- if (cause instanceof MQClientException clientException
- && clientException.getResponseCode() ==
ResponseCode.TOPIC_NOT_EXIST) {
- return true;
- }
- cause = cause.getCause() == cause ? null : cause.getCause();
- }
- return false;
- }
-
/**
* Trace lookup by business key. The key query already scopes the returned
trace messages to
* the requested message, so the body parser does not filter on a message
id. The original
@@ -496,6 +492,10 @@ public class RocketMQMessageProvider implements
MessageProvider {
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
+ if (MqResponseCodes.hasResponseCode(e,
ResponseCode.TOPIC_NOT_EXIST, ResponseCode.NO_MESSAGE)) {
+ log.info("No trace data available for key={} ({}), returning
empty trace", key, e.getMessage());
+ return emptyTrace();
+ }
log.warn("Trace query by key={} failed: {}", key, e.getMessage());
throw new BusinessException(502, "Failed to query message trace by
key: " + e.getMessage());
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/common/util/MqResponseCodesTest.java
b/server/src/test/java/org/apache/rocketmq/studio/common/util/MqResponseCodesTest.java
new file mode 100644
index 000000000..cfb229fa1
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/common/util/MqResponseCodesTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.apache.rocketmq.client.exception.MQBrokerException;
+import org.apache.rocketmq.client.exception.MQClientException;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class MqResponseCodesTest {
+
+ @Test
+ void matchesClientExceptionResponseCodeTest() {
+ MQClientException error = new
MQClientException(ResponseCode.NO_MESSAGE, "no message");
+ assertThat(MqResponseCodes.hasResponseCode(error,
+ ResponseCode.TOPIC_NOT_EXIST,
ResponseCode.NO_MESSAGE)).isTrue();
+ }
+
+ @Test
+ void matchesBrokerExceptionResponseCodeTest() {
+ MQBrokerException error = new
MQBrokerException(ResponseCode.CONSUMER_NOT_ONLINE, "not online");
+ assertThat(MqResponseCodes.hasResponseCode(error,
ResponseCode.CONSUMER_NOT_ONLINE)).isTrue();
+ }
+
+ @Test
+ void matchesResponseCodeOnNestedCauseTest() {
+ MQClientException cause = new
MQClientException(ResponseCode.TOPIC_NOT_EXIST, "no route");
+ RuntimeException wrapper = new RuntimeException("wrapper", cause);
+ assertThat(MqResponseCodes.hasResponseCode(wrapper,
ResponseCode.TOPIC_NOT_EXIST)).isTrue();
+ }
+
+ @Test
+ void returnsFalseWhenResponseCodeAbsentTest() {
+ MQClientException error = new
MQClientException(ResponseCode.TOPIC_NOT_EXIST, "no route");
+ assertThat(MqResponseCodes.hasResponseCode(error,
ResponseCode.NO_MESSAGE)).isFalse();
+ }
+
+ @Test
+ void returnsFalseForNullExceptionTest() {
+ assertThat(MqResponseCodes.hasResponseCode(null,
ResponseCode.NO_MESSAGE)).isFalse();
+ }
+
+ @Test
+ void terminatesOnSelfReferentialCauseTest() {
+ Throwable selfCaused = new Throwable("self") {
+ @Override
+ public synchronized Throwable getCause() {
+ return this;
+ }
+ };
+ assertThat(MqResponseCodes.hasResponseCode(selfCaused,
ResponseCode.NO_MESSAGE)).isFalse();
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
index e38d1524e..7812c46f2 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
@@ -20,6 +20,7 @@ import org.apache.rocketmq.client.QueryResult;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.consumer.PullStatus;
+import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.impl.MQClientAPIImpl;
import org.apache.rocketmq.client.impl.factory.MQClientInstance;
import org.apache.rocketmq.client.trace.TraceConstants;
@@ -27,6 +28,7 @@ import org.apache.rocketmq.common.message.MessageDecoder;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageId;
import org.apache.rocketmq.common.message.MessageQueue;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
import org.apache.rocketmq.remoting.protocol.body.CMResult;
@@ -213,6 +215,18 @@ class RocketMQMessageProviderTest {
.satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(502));
}
+ @Test
+ void queryByKeyReturnsEmptyListWhenClientReportsNoMessage() throws
Exception {
+ // MQAdminImpl.queryMessage throws MQClientException(NO_MESSAGE)
instead of
+ // returning an empty QueryResult when the key matches nothing.
+ when(adminExt.queryMessage("TopicA", "order-1", 64, 100L, 200L))
+ .thenThrow(new MQClientException(ResponseCode.NO_MESSAGE,
+ "query message by key finished, but no message."));
+
+ assertThat(provider.queryMessages(
+ "instance-a", "TopicA", null, null, "order-1", 100L,
200L)).isEmpty();
+ }
+
@Test
void queryByMsgIdUsesDecodedPhysicalOffsetForFallback() throws Exception {
String msgId = "AC1E0A6400002A9F0000000001A3F2B1";
@@ -631,6 +645,32 @@ class RocketMQMessageProviderTest {
}
+ @Test
+ void getMessageTraceReturnsEmptyTraceWhenClientReportsNoMessage() throws
Exception {
+ // A message without trace data (trace disabled on the producer or
expired) is a
+ // completed query with no records, not a remote failure.
+ when(adminExt.queryMessage(anyString(), anyString(), anyInt(),
anyLong(), anyLong()))
+ .thenThrow(new MQClientException(ResponseCode.NO_MESSAGE,
+ "query message by key finished, but no message."));
+
+ TraceRecordVO record = provider.getMessageTrace("instance-a",
"msg-123", "orders");
+
+ assertThat(record.getNodes()).isEmpty();
+ assertThat(record.getConsumerStatus()).isEmpty();
+ }
+
+ @Test
+ void getMessageTraceByKeyReturnsEmptyTraceWhenClientReportsNoMessage()
throws Exception {
+ when(adminExt.queryMessage(anyString(), anyString(), anyInt(),
anyLong(), anyLong()))
+ .thenThrow(new MQClientException(ResponseCode.NO_MESSAGE,
+ "query message by key finished, but no message."));
+
+ TraceRecordVO record = provider.getMessageTraceByKey("instance-a",
"key-1", "orders", null);
+
+ assertThat(record.getNodes()).isEmpty();
+ assertThat(record.getConsumerStatus()).isEmpty();
+ }
+
@Test
void getMessageTraceQueriesCustomTraceTopicWhenProvided() throws Exception
{
String pub = traceContext("Pub", "1000", "cn", "prod-group", "TopicA",
"msg-custom",