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 fbdcf867 fix: encode topic and consumer group path segments (#611)
fbdcf867 is described below

commit fbdcf867ad0f77f9505f7c17cc6c9cbe5333ca32
Author: yx9o <[email protected]>
AuthorDate: Tue Jul 28 20:52:18 2026 +0800

    fix: encode topic and consumer group path segments (#611)
---
 web/src/api/consumerGroups.test.ts | 19 ++++++++++++++++++-
 web/src/api/metadata.test.ts       | 18 +++++++++++++++++-
 web/src/api/metadata.ts            | 20 +++++++++++++++-----
 3 files changed, 50 insertions(+), 7 deletions(-)

diff --git a/web/src/api/consumerGroups.test.ts 
b/web/src/api/consumerGroups.test.ts
index 7cf943d9..c997a4d0 100644
--- a/web/src/api/consumerGroups.test.ts
+++ b/web/src/api/consumerGroups.test.ts
@@ -18,7 +18,13 @@
 import MockAdapter from 'axios-mock-adapter';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import client from './client';
-import { getConsumerGroup, listConsumerGroups, resetConsumerOffset } from 
'./metadata';
+import {
+  getConsumerGroup,
+  getConsumerProgress,
+  getConsumerSubscriptions,
+  listConsumerGroups,
+  resetConsumerOffset,
+} from './metadata';
 
 const mock = new MockAdapter(client);
 const group = {
@@ -59,6 +65,17 @@ describe('consumer groups API contract', () => {
     await expect(listConsumerGroups(params)).resolves.toEqual([group]);
   });
 
+  it('encodes consumer group names used in path segments', async () => {
+    const groupName = '%RETRY%cg-order';
+    mock.onGet('/groups/%25RETRY%25cg-order').reply(200, { code: 200, data: 
group });
+    mock.onGet('/groups/%25RETRY%25cg-order/progress').reply(200, { code: 200, 
data: [] });
+    mock.onGet('/groups/%25RETRY%25cg-order/subscriptions').reply(200, { code: 
200, data: [] });
+
+    await expect(getConsumerGroup(groupName)).resolves.toEqual(group);
+    await expect(getConsumerProgress(groupName)).resolves.toEqual([]);
+    await expect(getConsumerSubscriptions(groupName)).resolves.toEqual([]);
+  });
+
   it('unwraps detail records and sends numeric reset timestamps', async () => {
     const reset = { name: group.name, topic: 'orders', timestamp: 
1784246400000 };
     mock.onGet('/groups/orders').reply(200, { code: 200, data: group });
diff --git a/web/src/api/metadata.test.ts b/web/src/api/metadata.test.ts
index 4b18492d..3a57c1bc 100644
--- a/web/src/api/metadata.test.ts
+++ b/web/src/api/metadata.test.ts
@@ -18,7 +18,14 @@
 import MockAdapter from 'axios-mock-adapter';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import client from './client';
-import { createTopic, deleteTopic, listTopics, sendTopicMessage } from 
'./metadata';
+import {
+  createTopic,
+  deleteTopic,
+  getTopicConsumers,
+  getTopicRoutes,
+  listTopics,
+  sendTopicMessage,
+} from './metadata';
 
 const mock = new MockAdapter(client);
 
@@ -43,6 +50,15 @@ describe('topic metadata API', () => {
     await expect(listTopics(params)).resolves.toEqual([]);
   });
 
+  it('encodes topic names used in path segments', async () => {
+    const topicName = '%DLQ%cg-order';
+    mock.onGet('/topics/%25DLQ%25cg-order/routes').reply(200, { code: 200, 
data: [] });
+    mock.onGet('/topics/%25DLQ%25cg-order/consumers').reply(200, { code: 200, 
data: [] });
+
+    await expect(getTopicRoutes(topicName)).resolves.toEqual([]);
+    await expect(getTopicConsumers(topicName)).resolves.toEqual([]);
+  });
+
   it('persists topic creation, deletion, and sending through API endpoints', 
async () => {
     const topic = {
       name: 'orders',
diff --git a/web/src/api/metadata.ts b/web/src/api/metadata.ts
index abb45c91..bb85ec7b 100644
--- a/web/src/api/metadata.ts
+++ b/web/src/api/metadata.ts
@@ -124,12 +124,16 @@ export async function deleteTopic(name: string) {
 }
 
 export async function getTopicRoutes(name: string) {
-  const res = await client.get<{ data: BrokerRoute[] 
}>(`/topics/${name}/routes`);
+  const res = await client.get<{ data: BrokerRoute[] }>(
+    `/topics/${encodeURIComponent(name)}/routes`,
+  );
   return res.data.data;
 }
 
 export async function getTopicConsumers(name: string) {
-  const res = await client.get<{ data: ConsumerGroupInfo[] 
}>(`/topics/${name}/consumers`);
+  const res = await client.get<{ data: ConsumerGroupInfo[] }>(
+    `/topics/${encodeURIComponent(name)}/consumers`,
+  );
   return res.data.data;
 }
 
@@ -159,17 +163,23 @@ export async function listConsumerGroups(params?: 
ConsumerGroupQuery) {
 }
 
 export async function getConsumerGroup(name: string) {
-  const res = await client.get<{ data: ConsumerGroupDetail 
}>(`/groups/${name}`);
+  const res = await client.get<{ data: ConsumerGroupDetail }>(
+    `/groups/${encodeURIComponent(name)}`,
+  );
   return res.data.data;
 }
 
 export async function getConsumerProgress(name: string) {
-  const res = await client.get<{ data: QueueProgress[] 
}>(`/groups/${name}/progress`);
+  const res = await client.get<{ data: QueueProgress[] }>(
+    `/groups/${encodeURIComponent(name)}/progress`,
+  );
   return res.data.data;
 }
 
 export async function getConsumerSubscriptions(name: string) {
-  const res = await client.get<{ data: SubscriptionEntry[] 
}>(`/groups/${name}/subscriptions`);
+  const res = await client.get<{ data: SubscriptionEntry[] }>(
+    `/groups/${encodeURIComponent(name)}/subscriptions`,
+  );
   return res.data.data;
 }
 

Reply via email to