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 fd76d808e [ISSUE #2718] fix(ai): honor the configured API base for
streaming (#2724)
fd76d808e is described below
commit fd76d808ed6223c47b71bcfcac9675505ebac91e
Author: shown <[email protected]>
AuthorDate: Fri Sep 4 11:52:47 2026 +0800
[ISSUE #2718] fix(ai): honor the configured API base for streaming (#2724)
Signed-off-by: yuluo-yx <[email protected]>
---
web/src/api/ai.test.ts | 34 ++++++++++++++++++++++++++++++++--
web/src/api/ai.ts | 8 ++++++--
web/src/api/client.ts | 10 +++++++---
3 files changed, 45 insertions(+), 7 deletions(-)
diff --git a/web/src/api/ai.test.ts b/web/src/api/ai.test.ts
index 9ca698287..89b71029d 100644
--- a/web/src/api/ai.test.ts
+++ b/web/src/api/ai.test.ts
@@ -17,6 +17,7 @@
import MockAdapter from 'axios-mock-adapter';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { useAiChatHistoryStore } from '../stores/aiChatHistoryStore';
import client from './client';
import {
AiStreamError,
@@ -28,6 +29,10 @@ import {
type McpTool,
} from './ai';
+vi.mock('../config', () => ({
+ API_BASE_URL: 'https://backend.example.com/studio-api',
+}));
+
const mock = new MockAdapter(client);
const encoder = new TextEncoder();
@@ -45,6 +50,8 @@ function streamResponse(chunks: string[], onCancel?: () =>
void): Response {
describe('AI API', () => {
beforeEach(() => {
mock.reset();
+ localStorage.clear();
+ useAiChatHistoryStore.getState().clearHistories();
});
afterEach(() => {
@@ -53,7 +60,7 @@ describe('AI API', () => {
});
describe('chatStream (SSE)', () => {
- it('sends browser cookies without reading browser storage', async () => {
+ it('uses the configured API base URL and sends browser cookies', async ()
=> {
const fetchMock = vi.fn().mockResolvedValue(streamResponse(['data:
[DONE]\n\n']));
vi.stubGlobal('fetch', fetchMock);
@@ -62,7 +69,7 @@ describe('AI API', () => {
).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenCalledWith(
- '/api/ai/chat',
+ 'https://backend.example.com/studio-api/ai/chat',
expect.objectContaining({
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
@@ -70,6 +77,29 @@ describe('AI API', () => {
);
});
+ it('clears the current session when the stream request returns 401', async
() => {
+ localStorage.setItem('rocketmq-studio-user', 'admin');
+ localStorage.setItem('rocketmq-studio-user-admin', 'true');
+ useAiChatHistoryStore.getState().startConversation('real',
'conversation-1');
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn().mockResolvedValue(
+ new Response(JSON.stringify({ message: 'Unauthorized' }), {
+ status: 401,
+ statusText: 'Unauthorized',
+ }),
+ ),
+ );
+
+ await expect(
+ chatStream({ message: 'hello', mode: 'chat', model: 'stub' }, vi.fn()),
+ ).rejects.toMatchObject({ status: 401 });
+
+ expect(localStorage.getItem('rocketmq-studio-user')).toBeNull();
+ expect(localStorage.getItem('rocketmq-studio-user-admin')).toBeNull();
+
expect(useAiChatHistoryStore.getState().histories.real.conversations).toEqual([]);
+ });
+
it('reassembles an event split across network chunks', async () => {
vi.stubGlobal(
'fetch',
diff --git a/web/src/api/ai.ts b/web/src/api/ai.ts
index 8044adc53..564af6db0 100644
--- a/web/src/api/ai.ts
+++ b/web/src/api/ai.ts
@@ -15,7 +15,8 @@
* limitations under the License.
*/
-import client from './client';
+import { API_BASE_URL } from '../config';
+import client, { handleSessionUnauthorized } from './client';
const MAX_SSE_EVENT_CHARS = 1024 * 1024;
@@ -187,7 +188,7 @@ export async function chatStream(
signal?: AbortSignal,
onEnhance?: (prompt: string) => void,
) {
- const response = await fetch('/api/ai/chat', {
+ const response = await fetch(`${API_BASE_URL}/ai/chat`, {
method: 'POST',
credentials: 'include',
headers: {
@@ -197,6 +198,9 @@ export async function chatStream(
signal,
});
+ if (response.status === 401) {
+ handleSessionUnauthorized();
+ }
if (!response.ok) {
throw await parseHttpError(response);
}
diff --git a/web/src/api/client.ts b/web/src/api/client.ts
index d442c571a..0db0f5fb2 100644
--- a/web/src/api/client.ts
+++ b/web/src/api/client.ts
@@ -79,6 +79,12 @@ function isPublicAuthRequest(url?: string): boolean {
}
}
+export function handleSessionUnauthorized(): void {
+ clearAiChatHistories();
+ clearAuthSession();
+ window.location.href = '/login';
+}
+
const client = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
@@ -97,9 +103,7 @@ client.interceptors.response.use(
},
(error) => {
if (error.response?.status === 401 &&
!isPublicAuthRequest(error.config?.url)) {
- clearAiChatHistories();
- clearAuthSession();
- window.location.href = '/login';
+ handleSessionUnauthorized();
return Promise.reject(error);
}
if (isCorsRejection(error)) {