mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge pull request #918 from rdself/coder/fix-timeout-env-stream-300s
fix: make request and stream timeouts fully env-configurable
This commit is contained in:
17
.env.example
17
.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=*
|
||||
|
||||
30
README.md
30
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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string, Dispatcher>;
|
||||
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<typeof socksDispatcher>[0]
|
||||
socksOptions as Parameters<typeof socksDispatcher>[0],
|
||||
dispatcherOptions
|
||||
) as Dispatcher;
|
||||
} else {
|
||||
dispatcher = new ProxyAgent(normalizedUrl);
|
||||
dispatcher = new ProxyAgent({
|
||||
uri: normalizedUrl,
|
||||
...dispatcherOptions,
|
||||
});
|
||||
}
|
||||
|
||||
dispatcherCache.set(normalizedUrl, dispatcher);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
208
src/shared/utils/runtimeTimeouts.ts
Normal file
208
src/shared/utils/runtimeTimeouts.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
type EnvSource = Record<string, string | undefined>;
|
||||
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,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
80
tests/unit/runtime-timeouts.test.mjs
Normal file
80
tests/unit/runtime-timeouts.test.mjs
Normal file
@@ -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,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user