From 8b912380212386ac2b95e3f5ff906125065251d8 Mon Sep 17 00:00:00 2001 From: Beth Griggs Date: Tue, 8 Jul 2025 17:33:19 +0100 Subject: [PATCH] feat(http): support server-level timeout and socket options via app-config Adds support for configuring server-level HTTP options through the `app-config.yaml` file under the `backend.server` key. This includes support for: `headersTimeout`, `keepAliveTimeout`, `requestTimeout`, `timeout`, `maxHeadersCount`, and `maxRequestsPerSocket`. These options are passed directly to the underlying Node.js HTTP server, when omitted, the default values are used. Refs: https://github.com/backstage/backstage/issues/21808 Refs: https://github.com/backstage/backstage/issues/30449 Signed-off-by: Beth Griggs --- .changeset/salty-words-wash.md | 11 ++++ .../core-services/root-http-router.md | 8 +++ .../rootHttpRouter/http/config.test.ts | 53 +++++++++++++++++++ .../entrypoints/rootHttpRouter/http/config.ts | 30 +++++++++++ .../rootHttpRouter/http/createHttpServer.ts | 29 ++++++++-- .../entrypoints/rootHttpRouter/http/types.ts | 8 +++ 6 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 .changeset/salty-words-wash.md diff --git a/.changeset/salty-words-wash.md b/.changeset/salty-words-wash.md new file mode 100644 index 0000000000..eec2e85331 --- /dev/null +++ b/.changeset/salty-words-wash.md @@ -0,0 +1,11 @@ +--- +'@backstage/backend-defaults': minor +--- + +Adds support for configuring server-level HTTP options through the +`app-config.yaml` file under the `backend.server` key. Supported options +include `headersTimeout`, `keepAliveTimeout`, `requestTimeout`, `timeout`, +`maxHeadersCount`, and `maxRequestsPerSocket`. + +These are passed directly to the underlying Node.js HTTP server. +If omitted, Node.js defaults are used. diff --git a/docs/backend-system/core-services/root-http-router.md b/docs/backend-system/core-services/root-http-router.md index 1e76fb1871..0ced7aac66 100644 --- a/docs/backend-system/core-services/root-http-router.md +++ b/docs/backend-system/core-services/root-http-router.md @@ -65,6 +65,14 @@ backend: # - A standard ISO formatted duration string, e.g. 'P2DT6H' or 'PT1M'. # - An object with individual units (in plural) as keys, e.g. `{ days: 2, hours: 6 }`. serverShutdownDelay: { seconds: 20 } + server: + # (Optional) HTTP server configuration, Node.js defaults apply otherwise + headersTimeout: 60000 + keepAliveTimeout: 5000 + maxHeadersCount: 2000 + maxRequestsPerSocket: 100 + requestTimeout: 30000 + timeout: 30000 ``` ### Via Code diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.test.ts index 577bae08f3..0fa9dc0420 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.test.ts @@ -61,6 +61,59 @@ describe('readHttpServerOptions', () => { expect(readHttpServerOptions(new ConfigReader(input))).toEqual(output); }); + it.each([ + [ + { + server: { + headersTimeout: 10000, + requestTimeout: 30000, + keepAliveTimeout: 5000, + timeout: 60000, + maxHeadersCount: 1000, + maxRequestsPerSocket: 100, + }, + }, + { + listen: { host: '', port: 7007 }, + https: undefined, + serverOptions: { + headersTimeout: 10000, + requestTimeout: 30000, + keepAliveTimeout: 5000, + timeout: 60000, + maxHeadersCount: 1000, + maxRequestsPerSocket: 100, + }, + }, + ], + [ + { + server: { + keepAliveTimeout: 8000, + timeout: 30000, + }, + }, + { + listen: { host: '', port: 7007 }, + https: undefined, + serverOptions: { + keepAliveTimeout: 8000, + timeout: 30000, + }, + }, + ], + [ + { server: {} }, + { + listen: { host: '', port: 7007 }, + https: undefined, + serverOptions: undefined, + }, + ], + ])('should read server options %#', (input, output) => { + expect(readHttpServerOptions(new ConfigReader(input))).toEqual(output); + }); + it.each([ [ { listen: { port: 'not-a-number' } }, diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.ts index ec29a8a044..229ba2d78d 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.ts @@ -38,6 +38,7 @@ export function readHttpServerOptions(config?: Config): HttpServerOptions { return { listen: readHttpListenOptions(config), https: readHttpsOptions(config), + serverOptions: readServerOptions(config), }; } @@ -99,3 +100,32 @@ function readHttpsOptions(config?: Config): HttpServerOptions['https'] { }, }; } + +function readServerOptions( + config?: Config, +): HttpServerOptions['serverOptions'] { + const serverConfig = config?.getOptionalConfig('server'); + if (!serverConfig) { + return undefined; + } + + const serverOptions: HttpServerOptions['serverOptions'] = {}; + + const keys = [ + 'headersTimeout', + 'requestTimeout', + 'keepAliveTimeout', + 'timeout', + 'maxHeadersCount', + 'maxRequestsPerSocket', + ] as const; + + for (const key of keys) { + const value = serverConfig.getOptionalNumber(key); + if (value !== undefined) { + serverOptions[key] = value; + } + } + + return Object.keys(serverOptions).length === 0 ? undefined : serverOptions; +} diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/createHttpServer.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/createHttpServer.ts index fbb6ac5805..1ae6cc4fd1 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/createHttpServer.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/createHttpServer.ts @@ -84,6 +84,8 @@ async function createServer( options: HttpServerOptions, deps: { logger: LoggerService }, ): Promise { + let server: http.Server; + if (options.https) { const { certificate } = options.https; if (certificate.type === 'generated') { @@ -91,10 +93,31 @@ async function createServer( certificate.hostname, deps.logger, ); - return https.createServer(credentials, listener); + server = https.createServer(credentials, listener); + } else { + server = https.createServer(certificate, listener); } - return https.createServer(certificate, listener); + } else { + server = http.createServer(listener); } - return http.createServer(listener); + // apply custom server options + if (options.serverOptions) { + const { serverOptions } = options; + + if (serverOptions.headersTimeout !== undefined) + server.headersTimeout = serverOptions.headersTimeout; + if (serverOptions.requestTimeout !== undefined) + server.requestTimeout = serverOptions.requestTimeout; + if (serverOptions.keepAliveTimeout !== undefined) + server.keepAliveTimeout = serverOptions.keepAliveTimeout; + if (serverOptions.timeout !== undefined) + server.timeout = serverOptions.timeout; + if (serverOptions.maxHeadersCount !== undefined) + server.maxHeadersCount = serverOptions.maxHeadersCount; + if (serverOptions.maxRequestsPerSocket !== undefined) + server.maxRequestsPerSocket = serverOptions.maxRequestsPerSocket; + } + + return server; } diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/types.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/types.ts index 2298ef11ff..13e7914c03 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/types.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/types.ts @@ -42,6 +42,14 @@ export type HttpServerOptions = { https?: { certificate: HttpServerCertificateOptions; }; + serverOptions?: { + headersTimeout?: number; + requestTimeout?: number; + keepAliveTimeout?: number; + timeout?: number; + maxHeadersCount?: number; + maxRequestsPerSocket?: number; + }; }; /**