Skip to content

Commit 761d89c

Browse files
authored
fix(node-http-handler): undeprecate socket timeout (#1745)
* fix(node-http-handler): undeprecate socket timeout * set consistent error name with other timeouts
1 parent f634ef5 commit 761d89c

File tree

10 files changed

+190
-26
lines changed

10 files changed

+190
-26
lines changed

.changeset/hot-actors-roll.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/node-http-handler": minor
3+
"@smithy/types": minor
4+
---
5+
6+
undeprecate socketTimeout for node:https requests

packages/node-http-handler/src/node-http-handler.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"
55

66
import { NodeHttpHandler } from "./node-http-handler";
77
import * as setConnectionTimeoutModule from "./set-connection-timeout";
8+
import * as setRequestTimeoutModule from "./set-request-timeout";
89
import * as setSocketTimeoutModule from "./set-socket-timeout";
910
import { timing } from "./timing";
1011

@@ -57,6 +58,7 @@ describe("NodeHttpHandler", () => {
5758
const randomMaxSocket = Math.round(Math.random() * 50) + 1;
5859
const randomSocketAcquisitionWarningTimeout = Math.round(Math.random() * 10000) + 1;
5960
const randomConnectionTimeout = Math.round(Math.random() * 10000) + 1;
61+
const randomSocketTimeout = Math.round(Math.random() * 10000) + 1;
6062
const randomRequestTimeout = Math.round(Math.random() * 10000) + 1;
6163

6264
beforeEach(() => {});
@@ -140,10 +142,25 @@ describe("NodeHttpHandler", () => {
140142
}),
141143
],
142144
])("sets requestTimeout correctly when input is %s", async (_, option) => {
145+
vi.spyOn(setRequestTimeoutModule, "setRequestTimeout");
146+
const nodeHttpHandler = new NodeHttpHandler(option);
147+
await nodeHttpHandler.handle({} as any);
148+
expect(vi.mocked(setRequestTimeoutModule.setRequestTimeout).mock.calls[0][2]).toBe(randomRequestTimeout);
149+
});
150+
151+
it.each([
152+
["an options hash", { socketTimeout: randomSocketTimeout }],
153+
[
154+
"a provider",
155+
async () => ({
156+
socketTimeout: randomSocketTimeout,
157+
}),
158+
],
159+
])("sets socketTimeout correctly when input is %s", async (_, option) => {
143160
vi.spyOn(setSocketTimeoutModule, "setSocketTimeout");
144161
const nodeHttpHandler = new NodeHttpHandler(option);
145162
await nodeHttpHandler.handle({} as any);
146-
expect(vi.mocked(setSocketTimeoutModule.setSocketTimeout).mock.calls[0][2]).toBe(randomRequestTimeout);
163+
expect(vi.mocked(setSocketTimeoutModule.setSocketTimeout).mock.calls[0][2]).toBe(randomSocketTimeout);
147164
});
148165

149166
it.each([

packages/node-http-handler/src/node-http-handler.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Agent as hsAgent, request as hsRequest } from "https";
1010
import { NODEJS_TIMEOUT_ERROR_CODES } from "./constants";
1111
import { getTransformedHeaders } from "./get-transformed-headers";
1212
import { setConnectionTimeout } from "./set-connection-timeout";
13+
import { setRequestTimeout } from "./set-request-timeout";
1314
import { setSocketKeepAlive } from "./set-socket-keep-alive";
1415
import { setSocketTimeout } from "./set-socket-timeout";
1516
import { timing } from "./timing";
@@ -124,15 +125,24 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf
124125
}
125126

126127
private resolveDefaultConfig(options?: NodeHttpHandlerOptions | void): ResolvedNodeHttpHandlerConfig {
127-
const { requestTimeout, connectionTimeout, socketTimeout, socketAcquisitionWarningTimeout, httpAgent, httpsAgent } =
128-
options || {};
128+
const {
129+
requestTimeout,
130+
connectionTimeout,
131+
socketTimeout,
132+
socketAcquisitionWarningTimeout,
133+
httpAgent,
134+
httpsAgent,
135+
throwOnRequestTimeout,
136+
} = options || {};
129137
const keepAlive = true;
130138
const maxSockets = 50;
131139

132140
return {
133141
connectionTimeout,
134-
requestTimeout: requestTimeout ?? socketTimeout,
142+
requestTimeout,
143+
socketTimeout,
135144
socketAcquisitionWarningTimeout,
145+
throwOnRequestTimeout,
136146
httpAgent: (() => {
137147
if (httpAgent instanceof hAgent || typeof (httpAgent as hAgent)?.destroy === "function") {
138148
return httpAgent as hAgent;
@@ -288,7 +298,16 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf
288298
// are longer than a few seconds. This avoids slowing down faster operations.
289299
const effectiveRequestTimeout = requestTimeout ?? this.config.requestTimeout;
290300
timeouts.push(setConnectionTimeout(req, reject, this.config.connectionTimeout));
291-
timeouts.push(setSocketTimeout(req, reject, effectiveRequestTimeout));
301+
timeouts.push(
302+
setRequestTimeout(
303+
req,
304+
reject,
305+
effectiveRequestTimeout,
306+
this.config.throwOnRequestTimeout,
307+
this.config.logger ?? console
308+
)
309+
);
310+
timeouts.push(setSocketTimeout(req, reject, this.config.socketTimeout));
292311

293312
// Workaround for bug report in Node.js https://github.com/nodejs/node/issues/47137
294313
const httpAgent = nodeHttpsOptions.agent;

packages/node-http-handler/src/set-connection-timeout.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,14 @@ describe("setConnectionTimeout", () => {
7878
expect(clientRequest.destroy).toHaveBeenCalledTimes(1);
7979
expect(reject).toHaveBeenCalledTimes(1);
8080
expect(reject).toHaveBeenCalledWith(
81-
Object.assign(new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`), {
82-
name: "TimeoutError",
83-
})
81+
Object.assign(
82+
new Error(
83+
`@smithy/node-http-handler - the request socket did not establish a connection with the server within the configured timeout of ${timeoutInMs} ms.`
84+
),
85+
{
86+
name: "TimeoutError",
87+
}
88+
)
8489
);
8590
});
8691

packages/node-http-handler/src/set-connection-timeout.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ export const setConnectionTimeout = (
1818
const timeoutId = timing.setTimeout(() => {
1919
request.destroy();
2020
reject(
21-
Object.assign(new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`), {
22-
name: "TimeoutError",
23-
})
21+
Object.assign(
22+
new Error(
23+
`@smithy/node-http-handler - the request socket did not establish a connection with the server within the configured timeout of ${timeoutInMs} ms.`
24+
),
25+
{
26+
name: "TimeoutError",
27+
}
28+
)
2429
);
2530
}, timeoutInMs - offset);
2631

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { beforeEach, describe, expect, test as it, vi } from "vitest";
2+
3+
import { setRequestTimeout } from "./set-request-timeout";
4+
5+
describe("setRequestTimeout", () => {
6+
const reject = vi.fn();
7+
const clientRequest: any = {
8+
destroy: vi.fn(),
9+
};
10+
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
});
14+
15+
it("returns -1 if no timeout is given", () => {
16+
{
17+
const id = setRequestTimeout(clientRequest, reject, 0);
18+
expect(id).toEqual(-1);
19+
}
20+
{
21+
const id = setRequestTimeout(clientRequest, reject, undefined);
22+
expect(id).toEqual(-1);
23+
}
24+
});
25+
26+
describe("when timeout is provided", () => {
27+
it("rejects after the timeout", async () => {
28+
setRequestTimeout(clientRequest, reject, 1, true);
29+
await new Promise((r) => setTimeout(r, 2));
30+
expect(reject).toHaveBeenCalledWith(
31+
Object.assign(
32+
new Error(
33+
`@smithy/node-http-handler - [ERROR] a request has exceeded the configured ${1} ms requestTimeout.`
34+
),
35+
{
36+
name: "TimeoutError",
37+
code: "ETIMEDOUT",
38+
}
39+
)
40+
);
41+
expect(clientRequest.destroy).toHaveBeenCalled();
42+
});
43+
44+
it("logs a warning", async () => {
45+
const logger = {
46+
...console,
47+
warn: vi.fn(),
48+
};
49+
setRequestTimeout(clientRequest, reject, 1, false, logger);
50+
await new Promise((r) => setTimeout(r, 2));
51+
expect(logger.warn).toHaveBeenCalledWith(
52+
`@smithy/node-http-handler - [WARN] a request has exceeded the configured ${1} ms requestTimeout.` +
53+
` Init client requestHandler with throwOnRequestTimeout=true to turn this into an error.`
54+
);
55+
expect(clientRequest.destroy).not.toHaveBeenCalled();
56+
});
57+
});
58+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Logger } from "@smithy/types";
2+
import type { ClientRequest } from "http";
3+
4+
import { timing } from "./timing";
5+
6+
/**
7+
* @internal
8+
*/
9+
export const setRequestTimeout = (
10+
req: ClientRequest,
11+
reject: (err: Error) => void,
12+
timeoutInMs = 0,
13+
throwOnRequestTimeout?: boolean,
14+
logger?: Logger
15+
) => {
16+
if (timeoutInMs) {
17+
return timing.setTimeout(() => {
18+
let msg = `@smithy/node-http-handler - [${
19+
throwOnRequestTimeout ? "ERROR" : "WARN"
20+
}] a request has exceeded the configured ${timeoutInMs} ms requestTimeout.`;
21+
if (throwOnRequestTimeout) {
22+
const error = Object.assign(new Error(msg), {
23+
name: "TimeoutError",
24+
code: "ETIMEDOUT",
25+
});
26+
req.destroy(error);
27+
reject(error);
28+
} else {
29+
msg += ` Init client requestHandler with throwOnRequestTimeout=true to turn this into an error.`;
30+
logger?.warn?.(msg);
31+
}
32+
}, timeoutInMs);
33+
}
34+
return -1;
35+
};

packages/node-http-handler/src/set-socket-timeout.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ describe("setSocketTimeout", () => {
9090
clientRequest.setTimeout.mock.calls[0][1]();
9191
expect(reject).toHaveBeenCalledTimes(1);
9292
expect(reject).toHaveBeenCalledWith(
93-
Object.assign(new Error(`Connection timed out after ${timeoutInMs} ms`), { name: "TimeoutError" })
93+
Object.assign(
94+
new Error(
95+
`@smithy/node-http-handler - the request socket timed out after ${timeoutInMs} ms of inactivity (configured by client requestHandler).`
96+
),
97+
{ name: "TimeoutError" }
98+
)
9499
);
95100
});
96101
});

packages/node-http-handler/src/set-socket-timeout.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import type { ClientRequest } from "http";
22

3-
import { DEFAULT_REQUEST_TIMEOUT } from "./node-http-handler";
43
import { timing } from "./timing";
54

65
const DEFER_EVENT_LISTENER_TIME = 3000;
76

87
export const setSocketTimeout = (
98
request: ClientRequest,
109
reject: (err: Error) => void,
11-
timeoutInMs = DEFAULT_REQUEST_TIMEOUT
10+
timeoutInMs = 0
1211
): NodeJS.Timeout | number => {
1312
const registerTimeout = (offset: number) => {
1413
const timeout = timeoutInMs - offset;
1514
const onTimeout = () => {
1615
request.destroy();
17-
reject(Object.assign(new Error(`Connection timed out after ${timeoutInMs} ms`), { name: "TimeoutError" }));
16+
reject(
17+
Object.assign(
18+
new Error(
19+
`@smithy/node-http-handler - the request socket timed out after ${timeoutInMs} ms of inactivity (configured by client requestHandler).`
20+
),
21+
{ name: "TimeoutError" }
22+
)
23+
);
1824
};
1925

2026
if (request.socket) {

packages/types/src/http/httpHandlerInitialization.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,35 +26,43 @@ export interface NodeHttpHandlerOptions {
2626
/**
2727
* The maximum time in milliseconds that the connection phase of a request
2828
* may take before the connection attempt is abandoned.
29-
*
3029
* Defaults to 0, which disables the timeout.
3130
*/
3231
connectionTimeout?: number;
3332

3433
/**
35-
* The number of milliseconds a request can take before automatically being terminated.
34+
* The maximum number of milliseconds request & response should take.
3635
* Defaults to 0, which disables the timeout.
36+
*
37+
* If exceeded, a warning will be emitted unless throwOnRequestTimeout=true,
38+
* in which case a TimeoutError will be thrown.
3739
*/
3840
requestTimeout?: number;
3941

4042
/**
41-
* Delay before the NodeHttpHandler checks for socket exhaustion,
42-
* and emits a warning if the active sockets and enqueued request count is greater than
43-
* 2x the maxSockets count.
44-
*
45-
* Defaults to connectionTimeout + requestTimeout or 3000ms if those are not set.
43+
* Because requestTimeout was for a long time incorrectly being set as a socket idle timeout,
44+
* users must also opt-in for request timeout thrown errors.
45+
* Without this setting, a breach of the request timeout will be logged as a warning.
4646
*/
47-
socketAcquisitionWarningTimeout?: number;
47+
throwOnRequestTimeout?: boolean;
4848

4949
/**
50-
* This field is deprecated, and requestTimeout should be used instead.
5150
* The maximum time in milliseconds that a socket may remain idle before it
52-
* is closed.
51+
* is closed. Defaults to 0, which means no maximum.
5352
*
54-
* @deprecated Use {@link requestTimeout}
53+
* This does not affect the server, which may still close the connection due to an idle socket.
5554
*/
5655
socketTimeout?: number;
5756

57+
/**
58+
* Delay before the NodeHttpHandler checks for socket exhaustion,
59+
* and emits a warning if the active sockets and enqueued request count is greater than
60+
* 2x the maxSockets count.
61+
*
62+
* Defaults to connectionTimeout + requestTimeout or 3000ms if those are not set.
63+
*/
64+
socketAcquisitionWarningTimeout?: number;
65+
5866
/**
5967
* You can pass http.Agent or its constructor options.
6068
*/

0 commit comments

Comments
 (0)