From 9705818b15018eaccf0d022b2e87f1284d03ecf7 Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 3 Jun 2026 11:17:27 +0800 Subject: [PATCH] fix: wrap fetch for injecting headers --- src/app.tsx | 3 ++ src/pages/playground/apis/index.ts | 20 +++------ src/request.extensions.ts | 3 ++ src/utils/fetch-chunk-data.ts | 44 -------------------- src/utils/install-fetch.ts | 67 ++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 59 deletions(-) create mode 100644 src/utils/install-fetch.ts diff --git a/src/app.tsx b/src/app.tsx index 3cfe231b..3ec0c224 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -15,6 +15,7 @@ import { } from '@/services/profile/apis'; import { fetchSystemConfig } from '@/services/system/query-system-config'; import { isOnline } from '@/utils'; +import { installTenantFetch } from '@/utils/install-fetch'; import { IS_FIRST_LOGIN, readState, @@ -24,6 +25,8 @@ import '@gpustack/core-ui/style.css'; import { RequestConfig, history, request as umiRequest } from '@umijs/max'; import { message } from 'antd'; +installTenantFetch(); + // only for the first login and access from http://localhost const checkDefaultPage = async (userInfo: any) => { diff --git a/src/pages/playground/apis/index.ts b/src/pages/playground/apis/index.ts index c1b2bacc..1826fc23 100644 --- a/src/pages/playground/apis/index.ts +++ b/src/pages/playground/apis/index.ts @@ -1,9 +1,5 @@ import { GPUSTACK_API_BASE_URL, OPENAI_COMPATIBLE } from '@/config/settings'; -import { - createFormData, - errorHandler, - tenantHeaders -} from '@/utils/fetch-chunk-data'; +import { createFormData, errorHandler } from '@/utils/fetch-chunk-data'; import { request } from '@umijs/max'; export { GPUSTACK_API_BASE_URL, OPENAI_COMPATIBLE }; @@ -96,8 +92,7 @@ export const createImages = async ( body: JSON.stringify(params), signal: options.signal, headers: { - 'Content-Type': 'application/json', - ...tenantHeaders() + 'Content-Type': 'application/json' } }); if (!res.ok) { @@ -117,10 +112,7 @@ export const editImage = async (params: { const response = await fetch(EDIT_IMAGE_API, { method: 'POST', body: createFormData(params.data), - signal: params.signal, - headers: { - ...tenantHeaders() - } + signal: params.signal }); if (!response.ok) { return await errorHandler(response); @@ -137,8 +129,7 @@ export const createImage = async (params: { body: JSON.stringify(params.data), signal: params.signal, headers: { - 'Content-Type': 'application/json', - ...tenantHeaders() + 'Content-Type': 'application/json' } }); @@ -165,8 +156,7 @@ export const textToSpeech = async (params: any, options?: any) => { method: 'POST', body: JSON.stringify(params.data), headers: { - 'Content-Type': 'application/json', - ...tenantHeaders() + 'Content-Type': 'application/json' }, signal: params.signal }); diff --git a/src/request.extensions.ts b/src/request.extensions.ts index 8873a263..fa22f295 100644 --- a/src/request.extensions.ts +++ b/src/request.extensions.ts @@ -21,3 +21,6 @@ export const extraRequestInterceptors: RequestInterceptor[] = []; export type ResponseInterceptor = (response: any) => any; export const extraResponseInterceptors: ResponseInterceptor[] = []; +export const getTenantHeaders = ( + _method?: string +): Record => ({}); diff --git a/src/utils/fetch-chunk-data.ts b/src/utils/fetch-chunk-data.ts index 66618aa9..dd0adf62 100644 --- a/src/utils/fetch-chunk-data.ts +++ b/src/utils/fetch-chunk-data.ts @@ -3,48 +3,6 @@ import qs from 'query-string'; const extractStreamRegx = /(data|error):\s*({.*?})(?=\n|$)/g; -const readJsonNumber = ( - storage: Storage | null, - key: string -): number | null => { - if (storage == null) { - return null; - } - try { - const raw = storage.getItem(key); - if (raw == null) { - return null; - } - const parsed = JSON.parse(raw); - return typeof parsed === 'number' ? parsed : null; - } catch { - return null; - } -}; - -/** - * Read the active Org id the same way the umi request interceptor in - * ``request.extensions.ts`` does, then translate it to the header the - * backend's tenant resolver expects. Lets non-umi ``fetch()`` paths — - * streaming Playground completions, raw image / TTS POSTs — pin - * tenant context with the same precedence rules as everywhere else - * (createScope override first, current org second). Returns an empty - * object when no active org context is set. - */ -export const tenantHeaders = (): Record => { - if (typeof window === 'undefined') { - return {}; - } - try { - const orgId = - readJsonNumber(window.sessionStorage, 'createScopeOrgOverride') ?? - readJsonNumber(window.localStorage, 'currentOrganizationId'); - return orgId == null ? {} : { 'X-Organization-Id': String(orgId) }; - } catch { - return {}; - } -}; - const extractJSON = ( dataStr: string ): { results: any[]; remaining: string } => { @@ -113,7 +71,6 @@ export const fetchChunkedData = async (params: { signal: params.signal, headers: { 'Content-Type': 'application/json', - ...tenantHeaders(), ...(params.headers || {}) } }); @@ -167,7 +124,6 @@ export const fetchChunkedDataPostFormData = async (params: { body: createFormData(params.data), signal: params.signal, headers: { - ...tenantHeaders(), ...(params.headers || {}) } }); diff --git a/src/utils/install-fetch.ts b/src/utils/install-fetch.ts new file mode 100644 index 00000000..495219ad --- /dev/null +++ b/src/utils/install-fetch.ts @@ -0,0 +1,67 @@ +import { getTenantHeaders } from '@/request.extensions'; + +const isSameOrigin = (url: string): boolean => { + try { + // Relative URLs resolve against the current origin → same-origin. + return new URL(url, window.location.href).origin === window.location.origin; + } catch { + // Unparseable input — treat as a relative same-origin path. + return true; + } +}; + +const urlOf = (input: RequestInfo | URL): string => { + if (typeof input === 'string') return input; + if (input instanceof URL) return input.href; + return input.url; +}; + +const methodOf = (input: RequestInfo | URL, init?: RequestInit): string => { + if (init?.method) return init.method; + if (input instanceof Request) return input.method; + return 'GET'; +}; + +const mergeHeaders = ( + base: HeadersInit | undefined, + extra: Record +): Headers => { + const merged = new Headers(base ?? undefined); + Object.entries(extra).forEach(([key, value]) => merged.set(key, value)); + return merged; +}; + +let installed = false; + +export const installTenantFetch = (): void => { + if (installed || typeof window === 'undefined' || !window.fetch) { + return; + } + installed = true; + + const nativeFetch = window.fetch.bind(window); + + window.fetch = ( + input: RequestInfo | URL, + init?: RequestInit + ): Promise => { + const headers = getTenantHeaders(methodOf(input, init)); + if (Object.keys(headers).length === 0 || !isSameOrigin(urlOf(input))) { + return nativeFetch(input, init); + } + + // For a Request object the existing headers live on the object, not + // in `init`; fold them in so they survive the override. + if (input instanceof Request && init?.headers == null) { + return nativeFetch( + new Request(input, { headers: mergeHeaders(input.headers, headers) }), + init + ); + } + + return nativeFetch(input, { + ...init, + headers: mergeHeaders(init?.headers, headers) + }); + }; +};