From d1ebc5587149f246901be7fc94c895b3e3605713 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 4 Aug 2026 19:20:48 -0400 Subject: [PATCH] Bound public API connection lifetimes --- .env.example | 7 ++++++ docker-compose.external-db.yml | 5 ++++ docker-compose.yml | 5 ++++ full-setup/docker-compose-mainnet.yml | 5 ++++ src/main.ts | 33 ++++++++++++++++++++------- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index cb619e3..9662f4b 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,13 @@ API_PORT=3334 API_BIND_HOST=127.0.0.1 API_PUBLIC_PORT=3334 API_WORKERS=4 +# Bound abandoned or slow API sockets. These settings apply only to the +# HTTP(S) listener and do not affect Stratum connections. +API_CONNECTION_TIMEOUT_MS=15000 +API_KEEP_ALIVE_TIMEOUT_MS=5000 +API_REQUEST_TIMEOUT_MS=15000 +API_HEADERS_TIMEOUT_MS=10000 +API_MAX_REQUESTS_PER_SOCKET=100 # Docker json-file log rotation. Applies when using the compose files. DOCKER_LOG_MAX_SIZE=100m diff --git a/docker-compose.external-db.yml b/docker-compose.external-db.yml index 55eb93f..8c0f5a2 100644 --- a/docker-compose.external-db.yml +++ b/docker-compose.external-db.yml @@ -74,6 +74,11 @@ services: API_PORT: ${API_PORT:-3334} API_SECURE: ${API_SECURE:-false} API_WORKERS: ${API_WORKERS:-4} + API_CONNECTION_TIMEOUT_MS: ${API_CONNECTION_TIMEOUT_MS:-15000} + API_KEEP_ALIVE_TIMEOUT_MS: ${API_KEEP_ALIVE_TIMEOUT_MS:-5000} + API_REQUEST_TIMEOUT_MS: ${API_REQUEST_TIMEOUT_MS:-15000} + API_HEADERS_TIMEOUT_MS: ${API_HEADERS_TIMEOUT_MS:-10000} + API_MAX_REQUESTS_PER_SOCKET: ${API_MAX_REQUESTS_PER_SOCKET:-100} PM2_ENABLED: ${PM2_ENABLED:-true} STRATUM_WORKERS: ${STRATUM_WORKERS:-auto} STRATUM_PORTS: ${STRATUM_PORTS:-3333,3332,3331,3330} diff --git a/docker-compose.yml b/docker-compose.yml index ad1b758..f4b8ac5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -104,6 +104,11 @@ services: API_PORT: ${API_PORT:-3334} API_SECURE: ${API_SECURE:-false} API_WORKERS: ${API_WORKERS:-4} + API_CONNECTION_TIMEOUT_MS: ${API_CONNECTION_TIMEOUT_MS:-15000} + API_KEEP_ALIVE_TIMEOUT_MS: ${API_KEEP_ALIVE_TIMEOUT_MS:-5000} + API_REQUEST_TIMEOUT_MS: ${API_REQUEST_TIMEOUT_MS:-15000} + API_HEADERS_TIMEOUT_MS: ${API_HEADERS_TIMEOUT_MS:-10000} + API_MAX_REQUESTS_PER_SOCKET: ${API_MAX_REQUESTS_PER_SOCKET:-100} PM2_ENABLED: ${PM2_ENABLED:-true} STRATUM_WORKERS: ${STRATUM_WORKERS:-auto} STRATUM_PORTS: ${STRATUM_PORTS:-3333,3332,3331,3330} diff --git a/full-setup/docker-compose-mainnet.yml b/full-setup/docker-compose-mainnet.yml index f59777e..019b77d 100644 --- a/full-setup/docker-compose-mainnet.yml +++ b/full-setup/docker-compose-mainnet.yml @@ -108,6 +108,11 @@ services: DB_DATABASE: public_pool_mainnet REDIS_URL: redis://redis:6379 API_WORKERS: ${API_WORKERS:-4} + API_CONNECTION_TIMEOUT_MS: ${API_CONNECTION_TIMEOUT_MS:-15000} + API_KEEP_ALIVE_TIMEOUT_MS: ${API_KEEP_ALIVE_TIMEOUT_MS:-5000} + API_REQUEST_TIMEOUT_MS: ${API_REQUEST_TIMEOUT_MS:-15000} + API_HEADERS_TIMEOUT_MS: ${API_HEADERS_TIMEOUT_MS:-10000} + API_MAX_REQUESTS_PER_SOCKET: ${API_MAX_REQUESTS_PER_SOCKET:-100} PM2_ENABLED: "true" STRATUM_WORKERS: ${STRATUM_WORKERS:-2} STRATUM_MIN_DIFFICULTY: ${STRATUM_MIN_DIFFICULTY:-1} diff --git a/src/main.ts b/src/main.ts index 522320b..d31e6fa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,17 @@ import * as ecc from 'tiny-secp256k1'; import { ApiModule } from './api.module'; import { AppModule } from './app.module'; +const DEFAULT_API_CONNECTION_TIMEOUT_MS = 15_000; +const DEFAULT_API_KEEP_ALIVE_TIMEOUT_MS = 5_000; +const DEFAULT_API_REQUEST_TIMEOUT_MS = 15_000; +const DEFAULT_API_HEADERS_TIMEOUT_MS = 10_000; +const DEFAULT_API_MAX_REQUESTS_PER_SOCKET = 100; + +function readPositiveInt(name: string, fallback: number): number { + const value = Number(process.env[name]); + return Number.isInteger(value) && value > 0 ? value : fallback; +} + async function bootstrap() { if (process.env.API_PORT == null) { console.error('It appears your environment is not configured, create and populate an .env file.'); @@ -23,13 +34,18 @@ async function bootstrap() { const keyPath = path.join(currentDirectory, 'secrets', 'key.pem'); const certPath = path.join(currentDirectory, 'secrets', 'cert.pem'); - let options: any = {}; + let options: any = serveApi + ? { + connectionTimeout: readPositiveInt('API_CONNECTION_TIMEOUT_MS', DEFAULT_API_CONNECTION_TIMEOUT_MS), + keepAliveTimeout: readPositiveInt('API_KEEP_ALIVE_TIMEOUT_MS', DEFAULT_API_KEEP_ALIVE_TIMEOUT_MS), + requestTimeout: readPositiveInt('API_REQUEST_TIMEOUT_MS', DEFAULT_API_REQUEST_TIMEOUT_MS), + maxRequestsPerSocket: readPositiveInt('API_MAX_REQUESTS_PER_SOCKET', DEFAULT_API_MAX_REQUESTS_PER_SOCKET), + } + : {}; if (secure) { - options = { - https: { - key: readFileSync(keyPath), - cert: readFileSync(certPath), - } + options.https = { + key: readFileSync(keyPath), + cert: readFileSync(certPath), }; } @@ -75,11 +91,12 @@ async function bootstrap() { console.log(`API listening on ${address}`); }); + const server: any = app.getHttpServer(); + server.headersTimeout = readPositiveInt('API_HEADERS_TIMEOUT_MS', DEFAULT_API_HEADERS_TIMEOUT_MS); + // --- Live-reload TLS certs/keys when they change on disk --- if (secure) { // Fastify's underlying Node https server - const server: any = app.getHttpServer(); - // Guard: only HTTPS servers expose setSecureContext if (typeof server?.setSecureContext === 'function') { let reloadTimer: NodeJS.Timeout | null = null;