From aba27a7bbfc56c8f001ccbcfc6b998bc718f45bf Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 5 Aug 2026 17:53:28 -0400 Subject: [PATCH] support skip tls per request --- server/lib/aiGatewayUpstreamFetch.ts | 68 ++++++++++++++++++++++++++++ server/routers/aiGateway/pipeline.ts | 26 +++-------- 2 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 server/lib/aiGatewayUpstreamFetch.ts diff --git a/server/lib/aiGatewayUpstreamFetch.ts b/server/lib/aiGatewayUpstreamFetch.ts new file mode 100644 index 000000000..e41ec23e4 --- /dev/null +++ b/server/lib/aiGatewayUpstreamFetch.ts @@ -0,0 +1,68 @@ +import http from "node:http"; +import https from "node:https"; +import { Readable } from "node:stream"; + +type UpstreamFetchInit = { + method: string; + headers: Record; + body?: string; + skipTlsVerification?: boolean; +}; + +const insecureHttpsAgent = new https.Agent({ + rejectUnauthorized: false, + keepAlive: true +}); + +export function aiGatewayUpstreamFetch( + url: string, + init: UpstreamFetchInit +): Promise { + const parsed = new URL(url); + const isHttps = parsed.protocol === "https:"; + const lib = isHttps ? https : http; + const agent = + isHttps && init.skipTlsVerification ? insecureHttpsAgent : undefined; + + return new Promise((resolve, reject) => { + const req = lib.request( + url, + { + method: init.method, + headers: init.headers, + agent + }, + (res) => { + const headers = new Headers(); + for (const [key, value] of Object.entries(res.headers)) { + if (value === undefined) { + continue; + } + if (Array.isArray(value)) { + for (const entry of value) { + headers.append(key, entry); + } + } else { + headers.set(key, value); + } + } + + const body = Readable.toWeb(res) as ReadableStream; + resolve( + new Response(body, { + status: res.statusCode ?? 502, + statusText: res.statusMessage, + headers + }) + ); + } + ); + + req.on("error", reject); + + if (init.body !== undefined) { + req.write(init.body); + } + req.end(); + }); +} diff --git a/server/routers/aiGateway/pipeline.ts b/server/routers/aiGateway/pipeline.ts index 44e2aef66..a947f3d86 100644 --- a/server/routers/aiGateway/pipeline.ts +++ b/server/routers/aiGateway/pipeline.ts @@ -38,6 +38,7 @@ import { localCache } from "@server/lib/cache"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import type { ModelAccessMode } from "@server/lib/aiInferenceResource"; +import { aiGatewayUpstreamFetch } from "@server/lib/aiGatewayUpstreamFetch"; // Short-lived local caches so a burst of requests from the same IP/user // doesn't hit the database on every single request. None of this is @@ -544,15 +545,6 @@ export async function handleAiGatewayProxy( ); applyAiProviderAuthHeaders(headers, authType, apiKey); - // No dedicated per-request TLS agent is wired up (no extra deps for - // this v1 gateway) - toggle the process-wide Node TLS check instead. - // Known limitation: this is not safe under concurrent requests mixing - // skipTlsVerification providers with strict ones. - const restoreTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED; - if (provider.skipTlsVerification) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - } - const body = JSON.stringify(req.body); logger.debug("AI gateway upstream request", { @@ -560,15 +552,17 @@ export async function handleAiGatewayProxy( url: targetUrl, method: "POST", headers, - body: req.body + body: req.body, + skipTlsVerification: provider.skipTlsVerification }); let upstreamRes: globalThis.Response; try { - upstreamRes = await fetch(targetUrl, { + upstreamRes = await aiGatewayUpstreamFetch(targetUrl, { method: "POST", headers, - body + body, + skipTlsVerification: provider.skipTlsVerification }); } catch (fetchError) { logger.error({ @@ -581,14 +575,6 @@ export async function handleAiGatewayProxy( : undefined }); throw fetchError; - } finally { - if (provider.skipTlsVerification) { - if (restoreTlsReject === undefined) { - delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; - } else { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = restoreTlsReject; - } - } } const contentType = upstreamRes.headers.get("content-type") || "";