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 <bethanyngriggs@gmail.com>
This commit is contained in:
Beth Griggs
2025-07-08 17:33:19 +01:00
parent bd7e339ed8
commit 8b91238021
6 changed files with 136 additions and 3 deletions
+11
View File
@@ -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.
@@ -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
@@ -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' } },
@@ -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;
}
@@ -84,6 +84,8 @@ async function createServer(
options: HttpServerOptions,
deps: { logger: LoggerService },
): Promise<http.Server> {
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;
}
@@ -42,6 +42,14 @@ export type HttpServerOptions = {
https?: {
certificate: HttpServerCertificateOptions;
};
serverOptions?: {
headersTimeout?: number;
requestTimeout?: number;
keepAliveTimeout?: number;
timeout?: number;
maxHeadersCount?: number;
maxRequestsPerSocket?: number;
};
};
/**