Merge pull request #30483 from BethGriggs/declarative-http-options
feat(http): support server-level timeout and socket options via app-c…
This commit is contained in:
@@ -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,20 @@ 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
|
||||
# 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
|
||||
requestTimeout: '30s'
|
||||
keepAliveTimeout: { seconds: 5 }
|
||||
timeout: 'PT30S'
|
||||
# Numeric-only settings
|
||||
maxHeadersCount: 2000
|
||||
maxRequestsPerSocket: 100
|
||||
```
|
||||
|
||||
### 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.
|
||||
*/
|
||||
|
||||
+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' });
|
||||
});
|
||||
});
|
||||
|
||||
+69
@@ -117,6 +117,75 @@ 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.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 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