fix: CORS issues

This commit is contained in:
2026-08-15 12:26:15 +07:00
parent efe18629b1
commit 36c7261f92
2 changed files with 90 additions and 9 deletions
+50
View File
@@ -0,0 +1,50 @@
import { describe, it, expect, mock } from 'bun:test'
import { fetchRemoteModels } from './modelService'
describe('fetchRemoteModels', () => {
it('fetches models using Authorization header when supported', async () => {
const mockFetch = mock(() => {
return Promise.resolve(new Response(JSON.stringify({
data: [{ id: 'cx/gpt-5.6-sol', owned_by: 'cx' }]
}), { status: 200 }))
})
globalThis.fetch = mockFetch as unknown as typeof fetch
const models = await fetchRemoteModels({
baseUrl: 'https://gateway.example.com/v1',
apiKey: 'sk-test-key'
})
expect(models.length).toBe(1)
expect(models[0].id).toBe('cx/gpt-5.6-sol')
expect(models[0].provider).toBe('Codex / OpenAI')
})
it('falls back to key query param when preflight / Authorization header fails with CORS', async () => {
let callCount = 0
const mockFetch = mock((url: string | URL | Request) => {
callCount++
const urlStr = url.toString()
if (callCount === 1) {
// First attempt with Authorization header throws network/CORS TypeError
return Promise.reject(new TypeError('Failed to fetch'))
}
if (urlStr.includes('key=sk-test-key')) {
return Promise.resolve(new Response(JSON.stringify({
data: [{ id: 'cx/gpt-5.6-sol', owned_by: 'cx' }]
}), { status: 200 }))
}
return Promise.reject(new Error('Unexpected call'))
})
globalThis.fetch = mockFetch as unknown as typeof fetch
const models = await fetchRemoteModels({
baseUrl: 'https://gateway.example.com/v1',
apiKey: 'sk-test-key'
})
expect(models.length).toBe(1)
expect(models[0].id).toBe('cx/gpt-5.6-sol')
expect(callCount).toBe(2)
})
})
+40 -9
View File
@@ -61,13 +61,15 @@ export async function fetchRemoteModels(connection: ConnectionConfig): Promise<M
const trimmedBase = connection.baseUrl.replace(/\/+$/, '') const trimmedBase = connection.baseUrl.replace(/\/+$/, '')
let data: RawModelsResponse let data: RawModelsResponse
// Fetch the user-provided gateway directly. The gateway must permit this app's origin with CORS. const apiKey = connection.apiKey?.trim()
try { try {
// 1. First attempt: standard Authorization header
const headers: Record<string, string> = { const headers: Record<string, string> = {
Accept: 'application/json', Accept: 'application/json',
} }
if (connection.apiKey?.trim()) { if (apiKey) {
headers['Authorization'] = `Bearer ${connection.apiKey.trim()}` headers['Authorization'] = `Bearer ${apiKey}`
} }
const directRes = await fetch(`${trimmedBase}/models`, { const directRes = await fetch(`${trimmedBase}/models`, {
@@ -79,12 +81,41 @@ export async function fetchRemoteModels(connection: ConnectionConfig): Promise<M
throw new Error(`Gateway responded with ${directRes.status} ${directRes.statusText}`) throw new Error(`Gateway responded with ${directRes.status} ${directRes.statusText}`)
} }
data = await directRes.json() data = await directRes.json()
} catch (err: unknown) { } catch (firstErr: unknown) {
const reason = err instanceof Error ? err.message : 'Unknown network error' // 2. If an API key is provided and the standard header fetch failed (e.g. browser CORS preflight rejected Authorization header),
throw new Error( // fall back to passing the key as a query parameter (?key=...) which avoids custom header preflight issues.
`Unable to fetch models directly from the gateway. Check the Base URL, API key, and that the gateway allows this app origin with CORS. ${reason}`, if (apiKey) {
{ cause: err } try {
) const url = new URL(`${trimmedBase}/models`)
url.searchParams.set('key', apiKey)
const fallbackRes = await fetch(url.toString(), {
method: 'GET',
headers: {
Accept: 'application/json',
},
})
if (!fallbackRes.ok) {
throw new Error(`Gateway responded with ${fallbackRes.status} ${fallbackRes.statusText}`, {
cause: firstErr,
})
}
data = await fallbackRes.json()
} catch (fallbackErr: unknown) {
const reason = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown network error'
throw new Error(
`Unable to fetch models directly from the gateway. Check the Base URL, API key, and that the gateway allows this app origin with CORS. ${reason}`,
{ cause: fallbackErr }
)
}
} else {
const reason = firstErr instanceof Error ? firstErr.message : 'Unknown network error'
throw new Error(
`Unable to fetch models directly from the gateway. Check the Base URL, API key, and that the gateway allows this app origin with CORS. ${reason}`,
{ cause: firstErr }
)
}
} }
const rawList = Array.isArray(data) ? data : (data.data || []) const rawList = Array.isArray(data) ? data : (data.data || [])