feat(http): move HTTP server configuration application, enable flexible duration formats
This commit applies HTTP server configuration in `rootHttpRouterServiceFactory` and adds support for multiple duration formats for timeouts. The previous tests for server config have been moved and refactored. The documentation and CHANGELOG have been updated to reflect the new configuration structure and supported formats. Signed-off-by: Beth Griggs <bethanyngriggs@gmail.com>
This commit is contained in:
@@ -67,12 +67,18 @@ backend:
|
||||
serverShutdownDelay: { seconds: 20 }
|
||||
server:
|
||||
# (Optional) HTTP server configuration, Node.js defaults apply otherwise
|
||||
# Timeout values support multiple formats:
|
||||
# - Numbers (milliseconds): 30000
|
||||
# - Duration strings: '30s', '1 minute', '2 hours'
|
||||
# - ISO duration strings: 'PT30S', 'PT1M', 'PT2H'
|
||||
# - Duration objects: { seconds: 30 }, { minutes: 1 }, { hours: 2 }
|
||||
headersTimeout: 60000
|
||||
keepAliveTimeout: 5000
|
||||
requestTimeout: '30s'
|
||||
keepAliveTimeout: { seconds: 5 }
|
||||
timeout: 'PT30S'
|
||||
# Numeric-only settings
|
||||
maxHeadersCount: 2000
|
||||
maxRequestsPerSocket: 100
|
||||
requestTimeout: 30000
|
||||
timeout: 30000
|
||||
```
|
||||
|
||||
### Via Code
|
||||
|
||||
+25
@@ -95,6 +95,31 @@ export interface Config {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Server-level HTTP options configuration for the backend.
|
||||
* These options are passed directly to the underlying Node.js HTTP server.
|
||||
*
|
||||
* Timeout values support multiple formats:
|
||||
* - A number in milliseconds
|
||||
* - A string in the format of '1d', '2 seconds' etc. as supported by the `ms` library
|
||||
* - 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 }`
|
||||
*/
|
||||
server?: {
|
||||
/** Sets the timeout value for receiving the complete HTTP headers from the client. */
|
||||
headersTimeout?: number | string | HumanDuration;
|
||||
/** Sets the timeout value for receiving the entire request (headers and body) from the client. */
|
||||
requestTimeout?: number | string | HumanDuration;
|
||||
/** Sets the timeout value for inactivity on a socket during keep-alive. */
|
||||
keepAliveTimeout?: number | string | HumanDuration;
|
||||
/** Sets the timeout value for sockets. */
|
||||
timeout?: number | string | HumanDuration;
|
||||
/** Limits maximum incoming headers count. */
|
||||
maxHeadersCount?: number;
|
||||
/** Sets the maximum number of requests socket can handle before closing keep alive connection. */
|
||||
maxRequestsPerSocket?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options used by the default auditor service.
|
||||
*/
|
||||
|
||||
@@ -82,14 +82,6 @@ export type HttpServerOptions = {
|
||||
https?: {
|
||||
certificate: HttpServerCertificateOptions;
|
||||
};
|
||||
serverOptions?: {
|
||||
headersTimeout?: number;
|
||||
requestTimeout?: number;
|
||||
keepAliveTimeout?: number;
|
||||
timeout?: number;
|
||||
maxHeadersCount?: number;
|
||||
maxRequestsPerSocket?: number;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -61,59 +61,6 @@ 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,7 +38,6 @@ export function readHttpServerOptions(config?: Config): HttpServerOptions {
|
||||
return {
|
||||
listen: readHttpListenOptions(config),
|
||||
https: readHttpsOptions(config),
|
||||
serverOptions: readServerOptions(config),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,32 +99,3 @@ 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,8 +84,6 @@ 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') {
|
||||
@@ -93,31 +91,10 @@ async function createServer(
|
||||
certificate.hostname,
|
||||
deps.logger,
|
||||
);
|
||||
server = https.createServer(credentials, listener);
|
||||
} else {
|
||||
server = https.createServer(certificate, listener);
|
||||
return https.createServer(credentials, listener);
|
||||
}
|
||||
} else {
|
||||
server = http.createServer(listener);
|
||||
return https.createServer(certificate, 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;
|
||||
return http.createServer(listener);
|
||||
}
|
||||
|
||||
@@ -42,14 +42,6 @@ export type HttpServerOptions = {
|
||||
https?: {
|
||||
certificate: HttpServerCertificateOptions;
|
||||
};
|
||||
serverOptions?: {
|
||||
headersTimeout?: number;
|
||||
requestTimeout?: number;
|
||||
keepAliveTimeout?: number;
|
||||
timeout?: number;
|
||||
maxHeadersCount?: number;
|
||||
maxRequestsPerSocket?: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+61
@@ -323,4 +323,65 @@ describe('rootHttpRouterServiceFactory', () => {
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should start successfully with server configuration options', async () => {
|
||||
const { app } = await createExpressApp(
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: { port: 0 },
|
||||
server: {
|
||||
headersTimeout: 30000,
|
||||
requestTimeout: '45s',
|
||||
keepAliveTimeout: 'PT1M',
|
||||
timeout: { seconds: 120 },
|
||||
maxHeadersCount: 2000,
|
||||
maxRequestsPerSocket: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify the server starts and responds to health checks
|
||||
await request(app)
|
||||
.get('/.backstage/health/v1/liveness')
|
||||
.expect(200, { status: 'ok' });
|
||||
});
|
||||
|
||||
it('should start successfully with partial server configuration', async () => {
|
||||
const { app } = await createExpressApp(
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: { port: 0 },
|
||||
server: {
|
||||
headersTimeout: 60000,
|
||||
maxHeadersCount: 1500,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await request(app)
|
||||
.get('/.backstage/health/v1/liveness')
|
||||
.expect(200, { status: 'ok' });
|
||||
});
|
||||
|
||||
it('should start successfully with no server configuration', async () => {
|
||||
const { app } = await createExpressApp(
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: { port: 0 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await request(app)
|
||||
.get('/.backstage/health/v1/liveness')
|
||||
.expect(200, { status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
+77
@@ -117,6 +117,83 @@ const rootHttpRouterServiceFactoryWithOptions = (
|
||||
if (trustProxy !== undefined) {
|
||||
app.set('trust proxy', trustProxy);
|
||||
}
|
||||
|
||||
// Apply server-level HTTP options from config
|
||||
const backendConfig = config.getOptionalConfig('backend');
|
||||
const serverConfig = backendConfig?.getOptionalConfig('server');
|
||||
|
||||
if (serverConfig) {
|
||||
// Helper function to read duration values (supporting number, string, or HumanDuration)
|
||||
const readDurationValue = (key: string): number | undefined => {
|
||||
if (!serverConfig.has(key)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = serverConfig.getOptional(key);
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// If it's not a number, try to read it as a duration
|
||||
try {
|
||||
const duration = readDurationFromConfig(serverConfig, { key });
|
||||
return durationToMilliseconds(duration);
|
||||
} catch (error) {
|
||||
// Log warning for parsing failures
|
||||
logger.warn(
|
||||
`Failed to parse backend.server.${key} as duration: ${error}. ` +
|
||||
`Expected a number (milliseconds), duration string (e.g., '30s'), ` +
|
||||
`ISO duration (e.g., 'PT30S'), or duration object (e.g., {seconds: 30}). ` +
|
||||
`Falling back to number parsing.`,
|
||||
);
|
||||
// Fallback to reading as number if duration parsing fails
|
||||
const fallbackValue = serverConfig.getOptionalNumber(key);
|
||||
if (fallbackValue === undefined && typeof value === 'string') {
|
||||
logger.error(
|
||||
`backend.server.${key} value '${value}' could not be parsed as either ` +
|
||||
`a duration or a number. This setting will be ignored.`,
|
||||
);
|
||||
}
|
||||
return fallbackValue;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply timeout settings
|
||||
const headersTimeout = readDurationValue('headersTimeout');
|
||||
if (headersTimeout !== undefined) {
|
||||
server.headersTimeout = headersTimeout;
|
||||
}
|
||||
|
||||
const requestTimeout = readDurationValue('requestTimeout');
|
||||
if (requestTimeout !== undefined) {
|
||||
server.requestTimeout = requestTimeout;
|
||||
}
|
||||
|
||||
const keepAliveTimeout = readDurationValue('keepAliveTimeout');
|
||||
if (keepAliveTimeout !== undefined) {
|
||||
server.keepAliveTimeout = keepAliveTimeout;
|
||||
}
|
||||
|
||||
const timeout = readDurationValue('timeout');
|
||||
if (timeout !== undefined) {
|
||||
server.timeout = timeout;
|
||||
}
|
||||
|
||||
// Apply numeric settings
|
||||
const maxHeadersCount =
|
||||
serverConfig.getOptionalNumber('maxHeadersCount');
|
||||
if (maxHeadersCount !== undefined) {
|
||||
server.maxHeadersCount = maxHeadersCount;
|
||||
}
|
||||
|
||||
const maxRequestsPerSocket = serverConfig.getOptionalNumber(
|
||||
'maxRequestsPerSocket',
|
||||
);
|
||||
if (maxRequestsPerSocket !== undefined) {
|
||||
server.maxRequestsPerSocket = maxRequestsPerSocket;
|
||||
}
|
||||
}
|
||||
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
|
||||
Reference in New Issue
Block a user