mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<string, string>; body?: string },
|
||||
timeoutMs: number
|
||||
): Promise<{ status: number; ok: boolean; text: () => Promise<string> }> {
|
||||
return safeOutboundFetch(url, {
|
||||
method: options.method || "GET",
|
||||
headers: (options.headers || {}) as Record<string, string>,
|
||||
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<string, string> = {
|
||||
"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" };
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
59
tests/unit/nvidia-validation-bypass-proxy-3226.test.ts
Normal file
59
tests/unit/nvidia-validation-bypass-proxy-3226.test.ts
Normal file
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user