From 267de6d41f9c36e5b6a5e6f747c5be583ef0c70c Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Thu, 23 Jul 2026 11:30:47 -0600 Subject: [PATCH] fix(ai-alibaba-cloud): require clean api base urls --- packages/ai/alibaba-cloud/src/index.test.ts | 25 ++++++++++++++++++++- packages/ai/alibaba-cloud/src/index.ts | 19 +++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/ai/alibaba-cloud/src/index.test.ts b/packages/ai/alibaba-cloud/src/index.test.ts index 277faa4f..2d98aa3b 100644 --- a/packages/ai/alibaba-cloud/src/index.test.ts +++ b/packages/ai/alibaba-cloud/src/index.test.ts @@ -92,7 +92,7 @@ describe('Alibaba Cloud OpenAI-compatible generation', () => { vi.stubGlobal('fetch', fetchMock); await adapter.generate(ctx(), 'hello', { model: 'qwen-flash' }, { - baseUrl: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1', + baseUrl: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1/', }); expect(fetchMock).toHaveBeenCalledWith( @@ -101,6 +101,29 @@ describe('Alibaba Cloud OpenAI-compatible generation', () => { ); }); + it('rejects invalid or unclean custom base URLs before network calls', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'dashscope-us.aliyuncs.com/compatible-mode/v1' }), + ).rejects.toThrow('valid URL'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'ftp://dashscope-us.aliyuncs.com/compatible-mode/v1' }), + ).rejects.toThrow('http or https'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://user:pass@dashscope-us.aliyuncs.com/compatible-mode/v1' }), + ).rejects.toThrow('clean API base'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1?target=chat' }), + ).rejects.toThrow('clean API base'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1#chat' }), + ).rejects.toThrow('clean API base'); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + it('includes status and response body excerpt on errors', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, diff --git a/packages/ai/alibaba-cloud/src/index.ts b/packages/ai/alibaba-cloud/src/index.ts index a960d837..577ab797 100644 --- a/packages/ai/alibaba-cloud/src/index.ts +++ b/packages/ai/alibaba-cloud/src/index.ts @@ -30,7 +30,8 @@ export default defineAi({ if (opts.system) messages.push({ role: 'system', content: opts.system }); messages.push({ role: 'user', content: prompt }); - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/chat/completions`, { + const baseUrl = cleanBaseUrl(config.baseUrl ?? DEFAULT_BASE); + const res = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, @@ -70,6 +71,22 @@ export default defineAi({ }), }); +function cleanBaseUrl(value: string): string { + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error('Alibaba Cloud baseUrl must be a valid URL'); + } + if (url.protocol !== 'https:' && url.protocol !== 'http:') { + throw new Error('Alibaba Cloud baseUrl must use http or https'); + } + if (url.username || url.password || url.search || url.hash) { + throw new Error('Alibaba Cloud baseUrl must be a clean API base without credentials, query, or hash'); + } + return url.toString().replace(/\/+$/, ''); +} + type AlibabaCloudRole = 'system' | 'user' | 'assistant' | 'tool'; interface AlibabaCloudMessage {