Skip to content

Commit d0d2e08

Browse files
[Mon Apr 21 12:51:27 PM EDT 2025] Add keep alive timeouts; Enhance the reliability of connections.
1 parent 902c210 commit d0d2e08

File tree

13 files changed

+350
-273
lines changed

13 files changed

+350
-273
lines changed

app/constants/connection.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,44 @@ export const ConnectionEvent = <const>{
2323
};
2424

2525
/**
26-
* Default socket timeout in milliseconds (2 minutes)
26+
* Default socket timeout in milliseconds (30 seconds)
27+
*
28+
* Standard timeout for most socket connections.
29+
* It is long enough for slow clients but short enough to prevent idle connections from staying open indefinitely.
30+
*/
31+
export const DEFAULT_SOCKET_TIMEOUT = 30000;
32+
33+
/**
34+
* Default keep-alive timeout in milliseconds (65 seconds)
35+
*
36+
* This is the maximum time a connection can be idle before the server will close it.
37+
* This is to allow for a connection to stay open for a period of time so subsequent requests can be handled without a new connection.
38+
* This is also to prevent a connection from staying open indefinitely.
39+
* This is also useful for load balancing and preventing a single server from being overwhelmed by a large number of connections.
40+
* AWS recommends a keep-alive timeout of 65 seconds because there idle timeout is 60 seconds.
41+
*/
42+
export const DEFAULT_KEEP_ALIVE_TIMEOUT = 65000;
43+
44+
/**
45+
* Default headers timeout in milliseconds (66 seconds)
46+
*
47+
* This is the maximum time to wait for a header from the client.
48+
* This is to allow for a connection to stay open for a period of time so subsequent requests can be handled without a new connection.
49+
* This is also to prevent a connection from staying open indefinitely.
50+
* This is also useful for load balancing and preventing a single server from being overwhelmed by a large number of connections.
51+
* It is recommended to set this value to be greater than the keep-alive timeout to prevent the server from closing the connection prematurely
52+
* before the keep-alive timeout has expired.
53+
*/
54+
export const DEFAULT_HEADERS_TIMEOUT = 66000;
55+
56+
/**
57+
* Default graceful shutdown timeout in milliseconds (30 seconds)
58+
*
59+
* This is the maximum time to wait for a connection to complete before the server will close it.
60+
*/
61+
export const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = 30000;
62+
63+
/**
64+
* Default port for the server
2765
*/
28-
export const DEFAULT_SOCKET_TIMEOUT = 120000;
66+
export const DEFAULT_PORT = 5000;

app/core/ConfigManager.ts

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
import type { IServerOptions } from 'types/Server.ts';
2+
import type { TErrorFunction } from 'types/Response.ts';
3+
import type { Context } from 'core/Context.ts';
4+
import type { TResponseBody } from 'types/http/Response.ts';
5+
import {
6+
DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT,
7+
DEFAULT_HEADERS_TIMEOUT,
8+
DEFAULT_KEEP_ALIVE_TIMEOUT,
9+
DEFAULT_PORT,
10+
DEFAULT_SOCKET_TIMEOUT,
11+
} from 'constants/connection.ts';
12+
import { HttpStatusCode } from 'constants/http.ts';
213

314
/**
415
* Manages application configuration in a centralized way
@@ -8,44 +19,94 @@ import type { IServerOptions } from 'types/Server.ts';
819
* passing options through multiple layers.
920
*/
1021
export class ConfigManager {
11-
private readonly options: IServerOptions;
22+
readonly port: number;
23+
readonly errorHandler: TErrorFunction;
24+
readonly connectionOptions: Required<IServerOptions>['connectionOptions'];
25+
readonly parserOptions: Required<IServerOptions>['parserOptions'];
1226

1327
constructor(options?: IServerOptions) {
14-
this.options = options ?? {};
28+
this.port = this._setPort(options?.port);
29+
this.errorHandler = this._setErrorHandler(options?.errorHandler);
30+
this.connectionOptions = this._setConnectionOptions(options?.connectionOptions);
31+
this.parserOptions = this._setParserOptions(options?.parserOptions);
1532
}
1633

1734
/**
18-
* Get parser options
35+
* Sets the port number and normalizes it to a number
1936
*/
20-
get parserOptions(): IServerOptions['parserOptions'] {
21-
return this.options.parserOptions;
37+
private _setPort(port?: number | string): number {
38+
const normalizedPort = typeof port === 'string' ? parseInt(port, 10) : (port ?? DEFAULT_PORT);
39+
if (isNaN(normalizedPort) || normalizedPort < 0 || normalizedPort > 65535) {
40+
throw new Error('Invalid port number');
41+
}
42+
return normalizedPort;
2243
}
2344

2445
/**
25-
* Get connection options
46+
* Sets the error handler with validation
2647
*/
27-
get connectionOptions(): IServerOptions['connectionOptions'] {
28-
return this.options.connectionOptions;
48+
private _setErrorHandler(handler?: TErrorFunction): TErrorFunction {
49+
if (handler && typeof handler !== 'function') {
50+
throw new Error('Error handler must be a function');
51+
}
52+
return (
53+
handler ??
54+
(({ response }: Context, error: unknown): TResponseBody<unknown> => {
55+
console.error('Server error: \n', error);
56+
response.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR);
57+
if (error instanceof Error) {
58+
return { success: false, message: error.message };
59+
}
60+
return { success: false, message: 'An unknown error occurred' };
61+
})
62+
);
2963
}
3064

3165
/**
32-
* Get error handler
66+
* Sets and validates connection options
3367
*/
34-
get errorHandler(): IServerOptions['errorHandler'] {
35-
return this.options.errorHandler;
68+
private _setConnectionOptions(options?: IServerOptions['connectionOptions']): Required<IServerOptions>['connectionOptions'] {
69+
const normalizedOptions = {
70+
socketTimeout: this._normalizeTimeout(options?.socketTimeout, DEFAULT_SOCKET_TIMEOUT),
71+
gracefulShutdownTimeout: this._normalizeTimeout(options?.gracefulShutdownTimeout, DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT),
72+
keepAliveTimeout: this._normalizeTimeout(options?.keepAliveTimeout, DEFAULT_KEEP_ALIVE_TIMEOUT),
73+
headersTimeout: this._normalizeTimeout(options?.headersTimeout, DEFAULT_HEADERS_TIMEOUT),
74+
};
75+
76+
if (normalizedOptions.headersTimeout <= normalizedOptions.keepAliveTimeout) {
77+
throw new Error('headersTimeout must be greater than keepAliveTimeout');
78+
}
79+
80+
return normalizedOptions;
3681
}
3782

3883
/**
39-
* Get port
84+
* Normalizes a timeout value to a positive number
4085
*/
41-
get port(): number | undefined {
42-
return this.options.port;
86+
private _normalizeTimeout(value: number | string | undefined, defaultValue: number): number {
87+
const normalized = typeof value === 'string' ? parseInt(value, 10) : (value ?? defaultValue);
88+
if (isNaN(normalized) || normalized < 0) {
89+
throw new Error('Timeout must be a positive number');
90+
}
91+
return normalized;
4392
}
4493

4594
/**
46-
* Get the full options object
95+
* Sets and validates parser options
4796
*/
48-
getOptions(): IServerOptions {
49-
return { ...this.options };
97+
private _setParserOptions(options?: IServerOptions['parserOptions']): Required<IServerOptions>['parserOptions'] {
98+
const normalizedOptions = options ?? {};
99+
100+
// Validate JSON parser options
101+
if (normalizedOptions.json?.raw !== undefined && typeof normalizedOptions.json.raw !== 'boolean') {
102+
throw new Error('JSON parser raw option must be a boolean');
103+
}
104+
105+
// Validate YAML parser options
106+
if (normalizedOptions.yaml?.raw !== undefined && typeof normalizedOptions.yaml.raw !== 'boolean') {
107+
throw new Error('YAML parser raw option must be a boolean');
108+
}
109+
110+
return normalizedOptions;
50111
}
51112
}

app/core/ConnectionManager.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Server, Socket } from 'net';
22
import { EventEmitter } from 'events';
3-
import { ConnectionEvent, DEFAULT_SOCKET_TIMEOUT } from 'constants/connection.ts';
3+
import type { ConfigManager } from './ConfigManager.ts';
4+
import { ConnectionEvent } from 'constants/connection.ts';
45
import type { IConnectionStats } from 'types/Connection.ts';
56

67
/**
@@ -25,17 +26,17 @@ export class ConnectionManager extends EventEmitter {
2526
private _totalConnections = 0;
2627
/** Total connection errors */
2728
private _connectionErrors = 0;
28-
/** Default socket timeout in milliseconds */
29-
private readonly _socketTimeout: number;
29+
/** Config manager instance */
30+
private readonly _configManager: ConfigManager;
3031

3132
/**
3233
* Creates a new ConnectionManager instance
3334
*
34-
* @param socketTimeout Optional socket timeout in milliseconds (default: 2 minutes)
35+
* @param configManager The config manager instance
3536
*/
36-
constructor(socketTimeout?: number) {
37+
constructor(configManager: ConfigManager) {
3738
super();
38-
this._socketTimeout = socketTimeout ?? DEFAULT_SOCKET_TIMEOUT;
39+
this._configManager = configManager;
3940
}
4041

4142
/**
@@ -57,12 +58,17 @@ export class ConnectionManager extends EventEmitter {
5758
* Adds a socket connection to the manager and sets up event listeners
5859
*
5960
* @param socket The client socket connection
60-
* @param timeout Optional timeout for this specific connection
6161
*/
62-
addConnection(socket: Socket, timeout?: number): void {
62+
addConnection(socket: Socket): void {
6363
try {
64+
// Get socket timeout from config
65+
const { socketTimeout, keepAliveTimeout } = this._configManager.connectionOptions;
66+
6467
// Configure socket timeout
65-
socket.setTimeout(timeout ?? this._socketTimeout);
68+
socket.setTimeout(socketTimeout);
69+
70+
// Enable TCP keepalive
71+
socket.setKeepAlive(true, keepAliveTimeout);
6672

6773
// Add to active connections
6874
this._connections.add(socket);
@@ -83,7 +89,7 @@ export class ConnectionManager extends EventEmitter {
8389
});
8490

8591
socket.on('timeout', () => {
86-
// Destroy the socket on timeout
92+
// Send HTTP timeout response before closing
8793
socket.end('HTTP/1.1 408 Request Timeout\r\n\r\n');
8894
socket.destroy();
8995
});
@@ -164,10 +170,9 @@ export class ConnectionManager extends EventEmitter {
164170
/**
165171
* Closes all active connections gracefully
166172
*
167-
* @param gracePeriod Optional grace period in milliseconds before forcefully closing connections
168173
* @returns Promise that resolves when all connections are closed
169174
*/
170-
async closeAllConnections(gracePeriod?: number): Promise<void> {
175+
async closeAllConnections(): Promise<void> {
171176
// If there are no connections, resolve immediately
172177
if (this._connections.size === 0) {
173178
this.emit(ConnectionEvent.ALL_CONNECTIONS_CLOSED);
@@ -177,8 +182,11 @@ export class ConnectionManager extends EventEmitter {
177182
// Create a copy of the connections to avoid modification during iteration
178183
const connections = [...this._connections];
179184

185+
// Get graceful shutdown timeout from config
186+
const { gracefulShutdownTimeout } = this._configManager.connectionOptions;
187+
180188
// If grace period is provided, attempt graceful shutdown
181-
if (gracePeriod !== undefined && gracePeriod > 0) {
189+
if (gracefulShutdownTimeout > 0) {
182190
// End each connection with a proper HTTP response
183191
for (const socket of connections) {
184192
try {
@@ -190,7 +198,7 @@ export class ConnectionManager extends EventEmitter {
190198

191199
// Wait for the grace period
192200
await new Promise<void>((resolve) => {
193-
setTimeout(() => resolve(), gracePeriod);
201+
setTimeout(() => resolve(), gracefulShutdownTimeout);
194202
});
195203
}
196204

app/core/Request/RequestHandler.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class RequestHandler {
3030
this.routeFinder = routeFinder;
3131
this.hooksManager = hooksManager;
3232
this.configManager = configManager;
33-
this.errorHandler = configManager.errorHandler ?? this._defaultErrorHandler;
33+
this.errorHandler = configManager.errorHandler;
3434
}
3535

3636
/**
@@ -58,14 +58,7 @@ export class RequestHandler {
5858
}
5959
}
6060

61-
/**
62-
* Default error handler if none is provided
63-
*/
64-
private readonly _defaultErrorHandler: TErrorFunction = ({ response }, error): unknown => {
65-
console.error('Server error: \n', error);
66-
response.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR);
67-
return { success: false, message: 'Internal server error' };
68-
};
61+
6962

7063
/**
7164
* Sends an HTTP response through the socket

0 commit comments

Comments
 (0)