diff --git a/.env.example b/.env.example index a6c3713888..5ff8b6acc8 100644 --- a/.env.example +++ b/.env.example @@ -189,10 +189,21 @@ GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1 # Provider keys above (openai, mistral, together, fireworks, nvidia) also work for embeddings # Timeout settings -# FETCH_TIMEOUT_MS=120000 -# STREAM_IDLE_TIMEOUT_MS=60000 +# REQUEST_TIMEOUT_MS=600000 +# STREAM_IDLE_TIMEOUT_MS=600000 +# Advanced timeout overrides (optional) +# FETCH_TIMEOUT_MS=600000 +# FETCH_HEADERS_TIMEOUT_MS=600000 +# FETCH_BODY_TIMEOUT_MS=600000 +# FETCH_CONNECT_TIMEOUT_MS=30000 +# FETCH_KEEPALIVE_TIMEOUT_MS=4000 +# TLS_CLIENT_TIMEOUT_MS=600000 # API bridge timeout for /v1 proxy requests (default: 30000) -# API_BRIDGE_PROXY_TIMEOUT_MS=120000 +# API_BRIDGE_PROXY_TIMEOUT_MS=600000 +# API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS=600000 +# API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS=60000 +# API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS=5000 +# API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS=0 # CORS configuration (default: * allows all origins) # CORS_ORIGINS=* diff --git a/README.md b/README.md index c8c0b98faf..6b25e87197 100644 --- a/README.md +++ b/README.md @@ -803,6 +803,36 @@ PORT=20128 DASHBOARD_PORT=20129 omniroute # Dashboard: http://localhost:20129 ``` +### Long-Running Streaming Timeouts + +For most deployments, you only need: + +| Variable | Default | Purpose | +| ------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `REQUEST_TIMEOUT_MS` | `600000` | Shared baseline for upstream fetch, hidden Undici timeouts, TLS fingerprint requests, and API bridge request/proxy timeouts | +| `STREAM_IDLE_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` | Maximum gap between streaming chunks before OmniRoute aborts the SSE stream | + +Backward compatibility is preserved: existing `FETCH_TIMEOUT_MS`, `API_BRIDGE_PROXY_TIMEOUT_MS`, and other per-layer timeout vars still work and override the shared baseline. + +Advanced overrides are available if you need finer control: + +| Variable | Default | Purpose | +| ---------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------- | +| `FETCH_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` | Total upstream request timeout used by the main fetch abort signal | +| `FETCH_HEADERS_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Undici time limit for receiving upstream response headers | +| `FETCH_BODY_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Undici time limit between upstream body chunks (`0` disables it) | +| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | Undici TCP connect timeout | +| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Undici idle keep-alive socket timeout | +| `TLS_CLIENT_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Timeout for TLS fingerprint requests made through `wreq-js` | +| `API_BRIDGE_PROXY_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` or `30000` | Timeout for `/v1` proxy forwarding from API port to dashboard port | +| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `max(API_BRIDGE_PROXY_TIMEOUT_MS, 300000)` | Incoming request timeout on the API bridge server | +| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | Incoming header timeout on the API bridge server | +| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Keep-alive timeout on the API bridge server | +| `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | Socket inactivity timeout on the API bridge server (`0` disables it) | + +If you run OmniRoute behind Nginx, Caddy, Cloudflare, or another reverse proxy, make sure the proxy +timeouts are also higher than your OmniRoute stream/fetch timeouts. + ### 2) Connect providers and create your API key 1. Open Dashboard → `Providers` and connect at least one provider (OAuth or API key). diff --git a/docs/VM_DEPLOYMENT_GUIDE.md b/docs/VM_DEPLOYMENT_GUIDE.md index d5dce0cf30..ceee61ead4 100644 --- a/docs/VM_DEPLOYMENT_GUIDE.md +++ b/docs/VM_DEPLOYMENT_GUIDE.md @@ -213,8 +213,8 @@ server { # SSE (Server-Sent Events) — streaming AI responses proxy_buffering off; proxy_cache off; - proxy_read_timeout 300s; - proxy_send_timeout 300s; + proxy_read_timeout 600s; + proxy_send_timeout 600s; } } @@ -228,6 +228,10 @@ server { NGINX ``` +Keep reverse-proxy stream timeouts aligned with your OmniRoute timeout env vars. If you raise +`FETCH_TIMEOUT_MS` / `STREAM_IDLE_TIMEOUT_MS`, raise `proxy_read_timeout` / `proxy_send_timeout` +above the same threshold. + ### 3.3 Enable and Test ```bash diff --git a/open-sse/config/constants.ts b/open-sse/config/constants.ts index 7357d751b4..52ae37916e 100644 --- a/open-sse/config/constants.ts +++ b/open-sse/config/constants.ts @@ -1,18 +1,22 @@ +import { getUpstreamTimeoutConfig } from "@/shared/utils/runtimeTimeouts"; import { loadProviderCredentials } from "./credentialLoader.ts"; +import { generateLegacyProviders } from "./providerRegistry.ts"; + +const upstreamTimeouts = getUpstreamTimeoutConfig(process.env, (message) => { + console.warn(`[open-sse] ${message}`); +}); // Timeout for non-streaming fetch requests (ms). Prevents stalled connections. -export const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "600000", 10); +export const FETCH_TIMEOUT_MS = upstreamTimeouts.fetchTimeoutMs; // Idle timeout for SSE streams (ms). Closes stream if no data for this duration. // Default: 120s balances deep-reasoning pauses with fast zombie stream detection (#473). // Extended-thinking models rarely pause >90s between chunks. Override with STREAM_IDLE_TIMEOUT_MS env var. -export const STREAM_IDLE_TIMEOUT_MS = parseInt(process.env.STREAM_IDLE_TIMEOUT_MS || "600000", 10); +export const STREAM_IDLE_TIMEOUT_MS = upstreamTimeouts.streamIdleTimeoutMs; // Provider configurations // OAuth credentials read from env vars with hardcoded fallbacks for backward compatibility. // Use provider-credentials.json or env vars to override in production. -import { generateLegacyProviders } from "./providerRegistry.ts"; - export const PROVIDERS = generateLegacyProviders(); // Merge external credentials from data/provider-credentials.json (if present) diff --git a/open-sse/utils/proxyDispatcher.ts b/open-sse/utils/proxyDispatcher.ts index 8c76c57e1d..055c388b84 100644 --- a/open-sse/utils/proxyDispatcher.ts +++ b/open-sse/utils/proxyDispatcher.ts @@ -1,12 +1,15 @@ -import { ProxyAgent, type Dispatcher } from "undici"; +import { Agent, ProxyAgent, type Dispatcher } from "undici"; import { socksDispatcher } from "fetch-socks"; +import { getUpstreamTimeoutConfig } from "@/shared/utils/runtimeTimeouts"; const DISPATCHER_CACHE_KEY = Symbol.for("omniroute.proxyDispatcher.cache"); +const DEFAULT_DISPATCHER_KEY = Symbol.for("omniroute.proxyDispatcher.default"); const SUPPORTED_PROTOCOLS = new Set(["http:", "https:", "socks5:"]); type DispatcherCache = Map; type GlobalWithDispatcherCache = typeof globalThis & { [DISPATCHER_CACHE_KEY]?: DispatcherCache; + [DEFAULT_DISPATCHER_KEY]?: Dispatcher; }; type SocksDispatcherOptions = { type: number; @@ -38,6 +41,30 @@ function getDispatcherCache(): DispatcherCache { export function clearDispatcherCache() { const cache = getDispatcherCache(); cache.clear(); + + const globalWithCache = globalThis as GlobalWithDispatcherCache; + delete globalWithCache[DEFAULT_DISPATCHER_KEY]; +} + +function getDispatcherOptions() { + const timeouts = getUpstreamTimeoutConfig(process.env, (message) => { + console.warn(`[ProxyDispatcher] ${message}`); + }); + + return { + headersTimeout: timeouts.fetchHeadersTimeoutMs, + bodyTimeout: timeouts.fetchBodyTimeoutMs, + connectTimeout: timeouts.fetchConnectTimeoutMs, + keepAliveTimeout: timeouts.fetchKeepAliveTimeoutMs, + }; +} + +export function getDefaultDispatcher(): Dispatcher { + const globalWithCache = globalThis as GlobalWithDispatcherCache; + if (!globalWithCache[DEFAULT_DISPATCHER_KEY]) { + globalWithCache[DEFAULT_DISPATCHER_KEY] = new Agent(getDispatcherOptions()); + } + return globalWithCache[DEFAULT_DISPATCHER_KEY]; } /** @@ -180,6 +207,7 @@ export function proxyConfigToUrl( export function createProxyDispatcher(proxyUrl: string): Dispatcher { const normalizedUrl = normalizeProxyUrl(proxyUrl, "proxy dispatcher"); const dispatcherCache = getDispatcherCache(); + const dispatcherOptions = getDispatcherOptions(); let dispatcher = dispatcherCache.get(normalizedUrl); if (dispatcher) return dispatcher; @@ -197,10 +225,14 @@ export function createProxyDispatcher(proxyUrl: string): Dispatcher { if (parsed.username) socksOptions.userId = decodeURIComponent(parsed.username); if (parsed.password) socksOptions.password = decodeURIComponent(parsed.password); dispatcher = socksDispatcher( - socksOptions as Parameters[0] + socksOptions as Parameters[0], + dispatcherOptions ) as Dispatcher; } else { - dispatcher = new ProxyAgent(normalizedUrl); + dispatcher = new ProxyAgent({ + uri: normalizedUrl, + ...dispatcherOptions, + }); } dispatcherCache.set(normalizedUrl, dispatcher); diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index 09266a3438..c439cb4fb9 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -1,6 +1,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import { createProxyDispatcher, + getDefaultDispatcher, normalizeProxyUrl, proxyConfigToUrl, proxyUrlForLogs, @@ -196,7 +197,10 @@ async function patchedFetch(input: RequestInfo | URL, options: FetchWithDispatch if (store) store.used = false; } } - return originalFetchWithDispatcher(input, options); + return originalFetchWithDispatcher(input, { + ...options, + dispatcher: getDefaultDispatcher(), + }); } try { diff --git a/open-sse/utils/tlsClient.ts b/open-sse/utils/tlsClient.ts index dcb77a3d61..1674ed24c6 100644 --- a/open-sse/utils/tlsClient.ts +++ b/open-sse/utils/tlsClient.ts @@ -1,4 +1,5 @@ import { createRequire } from "module"; +import { getTlsClientTimeoutConfig } from "@/shared/utils/runtimeTimeouts"; const require = createRequire(import.meta.url); @@ -102,6 +103,9 @@ class TlsClient { async fetch(url: string, options: FetchOptions = {}) { const session = await this.getSession(); if (!session) throw new Error("wreq-js not available"); + const { timeoutMs } = getTlsClientTimeoutConfig(process.env, (message) => { + console.warn(`[TlsClient] ${message}`); + }); const method = (options.method || "GET").toUpperCase(); @@ -110,6 +114,7 @@ class TlsClient { headers: normalizeHeaders(options.headers), body: options.body, redirect: options.redirect === "manual" ? "manual" : "follow", + timeout: timeoutMs, }; // Pass signal through if available @@ -129,4 +134,6 @@ class TlsClient { } } -export default new TlsClient(); +const tlsClient = new TlsClient(); + +export default tlsClient; diff --git a/src/lib/apiBridgeServer.ts b/src/lib/apiBridgeServer.ts index d14bdada2e..ce521355c0 100644 --- a/src/lib/apiBridgeServer.ts +++ b/src/lib/apiBridgeServer.ts @@ -1,24 +1,11 @@ import http from "http"; import type { IncomingMessage, ServerResponse } from "http"; import { getRuntimePorts } from "@/lib/runtime/ports"; +import { getApiBridgeTimeoutConfig } from "@/shared/utils/runtimeTimeouts"; -const DEFAULT_PROXY_TIMEOUT_MS = 30_000; - -function parseProxyTimeoutMs(raw: string | undefined): number { - if (raw == null || raw.trim() === "") return DEFAULT_PROXY_TIMEOUT_MS; - const parsed = Number(raw); - - if (!Number.isFinite(parsed) || parsed <= 0) { - console.warn( - `[API Bridge] Invalid API_BRIDGE_PROXY_TIMEOUT_MS=\"${raw}\". Using default ${DEFAULT_PROXY_TIMEOUT_MS}ms.` - ); - return DEFAULT_PROXY_TIMEOUT_MS; - } - - return Math.floor(parsed); -} - -const PROXY_TIMEOUT_MS = parseProxyTimeoutMs(process.env.API_BRIDGE_PROXY_TIMEOUT_MS); +const API_BRIDGE_TIMEOUTS = getApiBridgeTimeoutConfig(process.env, (message) => { + console.warn(`[API Bridge] ${message}`); +}); const OPENAI_COMPAT_PATHS = [ /^\/v1(?:\/|$)/, @@ -45,7 +32,7 @@ function proxyRequest(req: IncomingMessage, res: ServerResponse, dashboardPort: ...req.headers, host: `127.0.0.1:${dashboardPort}`, }, - timeout: PROXY_TIMEOUT_MS, + timeout: API_BRIDGE_TIMEOUTS.proxyTimeoutMs, }, (targetRes) => { res.writeHead(targetRes.statusCode || 502, targetRes.headers); @@ -60,7 +47,7 @@ function proxyRequest(req: IncomingMessage, res: ServerResponse, dashboardPort: res.end( JSON.stringify({ error: "api_bridge_timeout", - detail: `Proxy request timed out after ${PROXY_TIMEOUT_MS}ms`, + detail: `Proxy request timed out after ${API_BRIDGE_TIMEOUTS.proxyTimeoutMs}ms`, }) ); }); @@ -112,6 +99,10 @@ export function initApiBridgeServer(): void { proxyRequest(req, res, dashboardPort); }); + server.requestTimeout = API_BRIDGE_TIMEOUTS.serverRequestTimeoutMs; + server.headersTimeout = API_BRIDGE_TIMEOUTS.serverHeadersTimeoutMs; + server.keepAliveTimeout = API_BRIDGE_TIMEOUTS.serverKeepAliveTimeoutMs; + server.setTimeout(API_BRIDGE_TIMEOUTS.serverSocketTimeoutMs); server.on("error", (error: NodeJS.ErrnoException) => { if (error?.code === "EADDRINUSE") { diff --git a/src/shared/utils/runtimeTimeouts.ts b/src/shared/utils/runtimeTimeouts.ts new file mode 100644 index 0000000000..9d5e39797b --- /dev/null +++ b/src/shared/utils/runtimeTimeouts.ts @@ -0,0 +1,208 @@ +type EnvSource = Record; +type TimeoutLogger = (message: string) => void; + +type ReadTimeoutOptions = { + allowZero?: boolean; + logger?: TimeoutLogger; +}; + +export const DEFAULT_FETCH_TIMEOUT_MS = 600_000; +export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 600_000; +export const DEFAULT_FETCH_CONNECT_TIMEOUT_MS = 30_000; +export const DEFAULT_FETCH_KEEPALIVE_TIMEOUT_MS = 4_000; +export const DEFAULT_API_BRIDGE_PROXY_TIMEOUT_MS = 30_000; +export const DEFAULT_API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS = 300_000; +export const DEFAULT_API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS = 60_000; +export const DEFAULT_API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS = 5_000; +export const DEFAULT_API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS = 0; + +function hasEnvValue(env: EnvSource, name: string): boolean { + const raw = env[name]; + return raw != null && raw.trim() !== ""; +} + +export type UpstreamTimeoutConfig = { + fetchTimeoutMs: number; + streamIdleTimeoutMs: number; + fetchHeadersTimeoutMs: number; + fetchBodyTimeoutMs: number; + fetchConnectTimeoutMs: number; + fetchKeepAliveTimeoutMs: number; +}; + +export type TlsClientTimeoutConfig = { + timeoutMs: number; +}; + +export type ApiBridgeTimeoutConfig = { + proxyTimeoutMs: number; + serverRequestTimeoutMs: number; + serverHeadersTimeoutMs: number; + serverKeepAliveTimeoutMs: number; + serverSocketTimeoutMs: number; +}; + +function readTimeoutMs( + env: EnvSource, + name: string, + defaultValue: number, + options: ReadTimeoutOptions = {} +): number { + const raw = env[name]; + if (raw == null || raw.trim() === "") return defaultValue; + + const parsed = Number(raw); + const isValid = Number.isFinite(parsed) && (options.allowZero ? parsed >= 0 : parsed > 0); + if (!isValid) { + options.logger?.(`Invalid ${name}="${raw}". Using default ${defaultValue}ms.`); + return defaultValue; + } + + return Math.floor(parsed); +} + +export function getUpstreamTimeoutConfig( + env: EnvSource = process.env, + logger?: TimeoutLogger +): UpstreamTimeoutConfig { + const sharedRequestTimeoutMs = hasEnvValue(env, "REQUEST_TIMEOUT_MS") + ? readTimeoutMs(env, "REQUEST_TIMEOUT_MS", DEFAULT_FETCH_TIMEOUT_MS, { + allowZero: true, + logger, + }) + : undefined; + const fetchTimeoutMs = readTimeoutMs( + env, + "FETCH_TIMEOUT_MS", + sharedRequestTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ); + const streamIdleTimeoutMs = readTimeoutMs( + env, + "STREAM_IDLE_TIMEOUT_MS", + sharedRequestTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ); + + return { + fetchTimeoutMs, + streamIdleTimeoutMs, + fetchHeadersTimeoutMs: readTimeoutMs(env, "FETCH_HEADERS_TIMEOUT_MS", fetchTimeoutMs, { + allowZero: true, + logger, + }), + fetchBodyTimeoutMs: readTimeoutMs(env, "FETCH_BODY_TIMEOUT_MS", fetchTimeoutMs, { + allowZero: true, + logger, + }), + fetchConnectTimeoutMs: readTimeoutMs( + env, + "FETCH_CONNECT_TIMEOUT_MS", + DEFAULT_FETCH_CONNECT_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ), + fetchKeepAliveTimeoutMs: readTimeoutMs( + env, + "FETCH_KEEPALIVE_TIMEOUT_MS", + DEFAULT_FETCH_KEEPALIVE_TIMEOUT_MS, + { + logger, + } + ), + }; +} + +export function getTlsClientTimeoutConfig( + env: EnvSource = process.env, + logger?: TimeoutLogger +): TlsClientTimeoutConfig { + const upstream = getUpstreamTimeoutConfig(env, logger); + + return { + timeoutMs: readTimeoutMs(env, "TLS_CLIENT_TIMEOUT_MS", upstream.fetchTimeoutMs, { + allowZero: true, + logger, + }), + }; +} + +export function getApiBridgeTimeoutConfig( + env: EnvSource = process.env, + logger?: TimeoutLogger +): ApiBridgeTimeoutConfig { + const sharedRequestTimeoutMs = hasEnvValue(env, "REQUEST_TIMEOUT_MS") + ? readTimeoutMs(env, "REQUEST_TIMEOUT_MS", DEFAULT_FETCH_TIMEOUT_MS, { + allowZero: true, + logger, + }) + : undefined; + const proxyTimeoutMs = readTimeoutMs( + env, + "API_BRIDGE_PROXY_TIMEOUT_MS", + sharedRequestTimeoutMs ?? DEFAULT_API_BRIDGE_PROXY_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ); + const derivedRequestTimeoutMs = + proxyTimeoutMs > 0 + ? Math.max(proxyTimeoutMs, DEFAULT_API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS) + : DEFAULT_API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS; + const serverKeepAliveTimeoutMs = readTimeoutMs( + env, + "API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS", + DEFAULT_API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ); + const serverHeadersTimeoutMs = readTimeoutMs( + env, + "API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS", + DEFAULT_API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ); + + return { + proxyTimeoutMs, + serverRequestTimeoutMs: readTimeoutMs( + env, + "API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS", + sharedRequestTimeoutMs + ? Math.max(sharedRequestTimeoutMs, derivedRequestTimeoutMs) + : derivedRequestTimeoutMs, + { + allowZero: true, + logger, + } + ), + serverHeadersTimeoutMs: + serverHeadersTimeoutMs > 0 && serverKeepAliveTimeoutMs > 0 + ? Math.max(serverHeadersTimeoutMs, serverKeepAliveTimeoutMs + 1_000) + : serverHeadersTimeoutMs, + serverKeepAliveTimeoutMs, + serverSocketTimeoutMs: readTimeoutMs( + env, + "API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS", + DEFAULT_API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS, + { + allowZero: true, + logger, + } + ), + }; +} diff --git a/tests/unit/runtime-timeouts.test.mjs b/tests/unit/runtime-timeouts.test.mjs new file mode 100644 index 0000000000..8a1c5e6ce7 --- /dev/null +++ b/tests/unit/runtime-timeouts.test.mjs @@ -0,0 +1,80 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const runtimeTimeouts = await import("../../src/shared/utils/runtimeTimeouts.ts"); + +test("upstream timeout config derives hidden fetch timeouts from FETCH_TIMEOUT_MS", () => { + const config = runtimeTimeouts.getUpstreamTimeoutConfig({ + FETCH_TIMEOUT_MS: "600000", + STREAM_IDLE_TIMEOUT_MS: "600000", + }); + + assert.deepEqual(config, { + fetchTimeoutMs: 600000, + streamIdleTimeoutMs: 600000, + fetchHeadersTimeoutMs: 600000, + fetchBodyTimeoutMs: 600000, + fetchConnectTimeoutMs: 30000, + fetchKeepAliveTimeoutMs: 4000, + }); +}); + +test("REQUEST_TIMEOUT_MS becomes the common timeout baseline when specific overrides are unset", () => { + const upstreamConfig = runtimeTimeouts.getUpstreamTimeoutConfig({ + REQUEST_TIMEOUT_MS: "600000", + }); + const apiBridgeConfig = runtimeTimeouts.getApiBridgeTimeoutConfig({ + REQUEST_TIMEOUT_MS: "600000", + }); + + assert.equal(upstreamConfig.fetchTimeoutMs, 600000); + assert.equal(upstreamConfig.streamIdleTimeoutMs, 600000); + assert.equal(upstreamConfig.fetchHeadersTimeoutMs, 600000); + assert.equal(upstreamConfig.fetchBodyTimeoutMs, 600000); + assert.equal(apiBridgeConfig.proxyTimeoutMs, 600000); + assert.equal(apiBridgeConfig.serverRequestTimeoutMs, 600000); +}); + +test("upstream timeout config honors explicit overrides and falls back on invalid values", () => { + const config = runtimeTimeouts.getUpstreamTimeoutConfig({ + REQUEST_TIMEOUT_MS: "550000", + FETCH_TIMEOUT_MS: "600000", + STREAM_IDLE_TIMEOUT_MS: "600000", + FETCH_HEADERS_TIMEOUT_MS: "610000", + FETCH_BODY_TIMEOUT_MS: "0", + FETCH_CONNECT_TIMEOUT_MS: "45000", + FETCH_KEEPALIVE_TIMEOUT_MS: "-1", + }); + + assert.equal(config.fetchHeadersTimeoutMs, 610000); + assert.equal(config.fetchBodyTimeoutMs, 0); + assert.equal(config.fetchConnectTimeoutMs, 45000); + assert.equal(config.fetchKeepAliveTimeoutMs, 4000); +}); + +test("TLS client timeout defaults to FETCH_TIMEOUT_MS and can be overridden", () => { + const defaultConfig = runtimeTimeouts.getTlsClientTimeoutConfig({ + FETCH_TIMEOUT_MS: "600000", + }); + const overriddenConfig = runtimeTimeouts.getTlsClientTimeoutConfig({ + FETCH_TIMEOUT_MS: "600000", + TLS_CLIENT_TIMEOUT_MS: "720000", + }); + + assert.equal(defaultConfig.timeoutMs, 600000); + assert.equal(overriddenConfig.timeoutMs, 720000); +}); + +test("API bridge timeouts align request timeout with long proxy timeout by default", () => { + const config = runtimeTimeouts.getApiBridgeTimeoutConfig({ + API_BRIDGE_PROXY_TIMEOUT_MS: "600000", + }); + + assert.deepEqual(config, { + proxyTimeoutMs: 600000, + serverRequestTimeoutMs: 600000, + serverHeadersTimeoutMs: 60000, + serverKeepAliveTimeoutMs: 5000, + serverSocketTimeoutMs: 0, + }); +});