From 4dbbbaacf19a1bd50d4e0b69c5a7fb20009207dd Mon Sep 17 00:00:00 2001 From: MeAdityaB <104827794+miracuves@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:22:27 +0530 Subject: [PATCH] fix: NVIDIA NIM API key validation timeout (bypass proxy fetch patch) (#3226) NVIDIA NIM validation bypasses the proxy-patched fetch (504 fix) + combined with #3116 reliable probe model + test. Integrated into release/v3.8.11. --- open-sse/utils/proxyFetch.ts | 10 ++++ src/lib/providers/validation.ts | 32 +++++++++- src/shared/network/safeOutboundFetch.ts | 19 ++++-- src/shared/utils/fetchTimeout.ts | 8 ++- ...vidia-validation-bypass-proxy-3226.test.ts | 59 +++++++++++++++++++ 5 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 tests/unit/nvidia-validation-bypass-proxy-3226.test.ts diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index 9a1a3461f7..3b3e7501a1 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -453,4 +453,14 @@ export function isTlsFingerprintActive() { return isTlsFingerprintEnabled() && tlsClient.available; } +/** + * Get the original unpatched global fetch function (Node.js native fetch + * before the proxy/TLS fingerprint patch was applied). + * Use this to bypass the patched fetch for specific requests when the + * proxy dispatcher has compatibility issues with a particular endpoint. + */ +export function getOriginalFetch(): typeof globalThis.fetch { + return originalFetch; +} + export default isCloud ? originalFetch : patchedFetch; diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index ec68ddeb30..55a61e324b 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -211,6 +211,32 @@ function withCustomUserAgent(init: RequestInit, providerSpecificData: any = {}) }; } +/** + * Direct HTTPS request utility that bypasses the global patched fetch. + * Used for provider validation where the patched fetch has compatibility issues. + * Uses safeOutboundFetch with bypassProxyPatch to use native Node.js fetch directly. + */ +function directHttpsRequest( + url: string, + options: { method?: string; headers?: Record; body?: string }, + timeoutMs: number +): Promise<{ status: number; ok: boolean; text: () => Promise }> { + return safeOutboundFetch(url, { + method: options.method || "GET", + headers: (options.headers || {}) as Record, + body: options.body, + timeoutMs, + bypassProxyPatch: true, + allowRedirect: true, + guard: "none", + retry: false, + }).then(async (response) => ({ + status: response.status, + ok: response.ok, + text: async () => await response.text(), + })); +} + function buildBearerHeaders(apiKey: string, providerSpecificData: any = {}) { const headers: Record = { "Content-Type": "application/json", @@ -3966,7 +3992,9 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi // (z-ai/glm-5.1), which requires the "Public API Endpoints" account permission // and can hang/be DEGRADED — making a *valid* key fail with "Upstream Error". const modelId = resolveNvidiaValidationModel(providerSpecificData); - const res = await validationWrite( + // #3226: use raw https (bypass the proxy/TLS-patched fetch) — the undici + // dispatcher stalls against NVIDIA's endpoint, causing a 504 timeout. + const res = await directHttpsRequest( chatUrl, { method: "POST", @@ -3977,7 +4005,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi max_tokens: 1, }), }, - isLocal + 20000 ); if (res.status === 401 || res.status === 403) { return { valid: false, error: "Invalid API key" }; diff --git a/src/shared/network/safeOutboundFetch.ts b/src/shared/network/safeOutboundFetch.ts index 89e2f35416..7638ee44c1 100644 --- a/src/shared/network/safeOutboundFetch.ts +++ b/src/shared/network/safeOutboundFetch.ts @@ -1,4 +1,4 @@ -import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts"; +import { runWithProxyContext, getOriginalFetch } from "@omniroute/open-sse/utils/proxyFetch.ts"; import { FetchTimeoutError, fetchWithTimeout } from "@/shared/utils/fetchTimeout"; import { OutboundUrlGuardError, @@ -30,6 +30,10 @@ export interface SafeOutboundFetchOptions extends RequestInit { retry?: SafeOutboundFetchRetryOptions | false; guard?: SafeOutboundFetchGuard; proxyConfig?: unknown; + /** Bypass the global proxy/TLS patched fetch and use the native Node.js + * fetch directly. Use when a provider endpoint has compatibility issues + * with the undici dispatcher layer. */ + bypassProxyPatch?: boolean; } type SafeOutboundFetchPresetMap = { @@ -51,7 +55,7 @@ export const SAFE_OUTBOUND_FETCH_PRESETS: SafeOutboundFetchPresetMap = { }, }, validationWrite: { - timeoutMs: 7000, + timeoutMs: 15000, allowRedirect: false, retry: false, }, @@ -264,6 +268,7 @@ export async function safeOutboundFetch(url: string | URL, options: SafeOutbound retry, guard = "none", proxyConfig, + bypassProxyPatch = false, signal, ...fetchOptions } = options; @@ -282,11 +287,15 @@ export async function safeOutboundFetch(url: string | URL, options: SafeOutbound redirect, signal, timeoutMs, + // When bypassing the proxy patch, use the original native fetch directly. + fetchFn: bypassProxyPatch ? getOriginalFetch() : undefined, }); - const response = proxyConfig - ? await runWithProxyContext(proxyConfig, executeFetch) - : await executeFetch(); + const response = bypassProxyPatch + ? await executeFetch() + : proxyConfig + ? await runWithProxyContext(proxyConfig, executeFetch) + : await executeFetch(); if (!allowRedirect && response.status >= 300 && response.status < 400) { const location = response.headers.get("location"); diff --git a/src/shared/utils/fetchTimeout.ts b/src/shared/utils/fetchTimeout.ts index 55efe599ac..466364ff88 100644 --- a/src/shared/utils/fetchTimeout.ts +++ b/src/shared/utils/fetchTimeout.ts @@ -13,10 +13,13 @@ const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "", 10) || DEF interface FetchTimeoutOptions extends RequestInit { timeoutMs?: number; + /** Alternative fetch function to use instead of globalThis.fetch. + * Pass getOriginalFetch() to bypass the proxy/TLS patch layer. */ + fetchFn?: typeof globalThis.fetch; } export async function fetchWithTimeout(url: string | URL, options: FetchTimeoutOptions = {}) { - const { timeoutMs = FETCH_TIMEOUT_MS, signal: externalSignal, ...fetchOptions } = options; + const { timeoutMs = FETCH_TIMEOUT_MS, signal: externalSignal, fetchFn, ...fetchOptions } = options; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); @@ -31,7 +34,8 @@ export async function fetchWithTimeout(url: string | URL, options: FetchTimeoutO } try { - const response = await fetch(url, { + const doFetch = fetchFn || globalThis.fetch; + const response = await doFetch(url, { ...fetchOptions, signal: controller.signal, }); diff --git a/tests/unit/nvidia-validation-bypass-proxy-3226.test.ts b/tests/unit/nvidia-validation-bypass-proxy-3226.test.ts new file mode 100644 index 0000000000..78a113e710 --- /dev/null +++ b/tests/unit/nvidia-validation-bypass-proxy-3226.test.ts @@ -0,0 +1,59 @@ +/** + * #3226 — NVIDIA NIM validation must be able to bypass the global proxy/TLS + * patched fetch (the undici dispatcher stalls against NVIDIA's endpoint → 504). + * + * The mechanism: `safeOutboundFetch({ bypassProxyPatch: true })` resolves the + * native fetch via `getOriginalFetch()` and threads it to `fetchWithTimeout` + * as `fetchFn`, which calls it instead of the (patched) `globalThis.fetch`. + * These tests guard the wiring at its two seams. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { fetchWithTimeout } = await import("../../src/shared/utils/fetchTimeout.ts"); +const { getOriginalFetch } = await import("../../open-sse/utils/proxyFetch.ts"); + +const originalGlobalFetch = globalThis.fetch; + +test.afterEach(() => { + globalThis.fetch = originalGlobalFetch; +}); + +test("#3226 fetchWithTimeout uses the provided fetchFn instead of globalThis.fetch", async () => { + let globalCalled = false; + let fnCalled = false; + globalThis.fetch = async () => { + globalCalled = true; + return Response.json({ via: "global" }); + }; + const customFetch = (async () => { + fnCalled = true; + return Response.json({ via: "custom" }); + }) as unknown as typeof globalThis.fetch; + + const res = await fetchWithTimeout("https://integrate.api.nvidia.com/v1/chat/completions", { + method: "POST", + timeoutMs: 100, + fetchFn: customFetch, + }); + + assert.equal(fnCalled, true, "provided fetchFn must be used"); + assert.equal(globalCalled, false, "patched globalThis.fetch must be bypassed"); + assert.deepEqual(await res.json(), { via: "custom" }); +}); + +test("#3226 fetchWithTimeout falls back to globalThis.fetch when no fetchFn is given", async () => { + let globalCalled = false; + globalThis.fetch = async () => { + globalCalled = true; + return Response.json({ via: "global" }); + }; + + await fetchWithTimeout("https://example.test/x", { method: "GET", timeoutMs: 100 }); + assert.equal(globalCalled, true); +}); + +test("#3226 getOriginalFetch returns a callable native fetch (the un-patched reference)", () => { + const native = getOriginalFetch(); + assert.equal(typeof native, "function"); +});