Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hot-actors-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@smithy/node-http-handler": minor
"@smithy/types": minor
---

undeprecate socketTimeout for node:https requests
19 changes: 18 additions & 1 deletion packages/node-http-handler/src/node-http-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"

import { NodeHttpHandler } from "./node-http-handler";
import * as setConnectionTimeoutModule from "./set-connection-timeout";
import * as setRequestTimeoutModule from "./set-request-timeout";
import * as setSocketTimeoutModule from "./set-socket-timeout";
import { timing } from "./timing";

Expand Down Expand Up @@ -57,6 +58,7 @@ describe("NodeHttpHandler", () => {
const randomMaxSocket = Math.round(Math.random() * 50) + 1;
const randomSocketAcquisitionWarningTimeout = Math.round(Math.random() * 10000) + 1;
const randomConnectionTimeout = Math.round(Math.random() * 10000) + 1;
const randomSocketTimeout = Math.round(Math.random() * 10000) + 1;
const randomRequestTimeout = Math.round(Math.random() * 10000) + 1;

beforeEach(() => {});
Expand Down Expand Up @@ -140,10 +142,25 @@ describe("NodeHttpHandler", () => {
}),
],
])("sets requestTimeout correctly when input is %s", async (_, option) => {
vi.spyOn(setRequestTimeoutModule, "setRequestTimeout");
const nodeHttpHandler = new NodeHttpHandler(option);
await nodeHttpHandler.handle({} as any);
expect(vi.mocked(setRequestTimeoutModule.setRequestTimeout).mock.calls[0][2]).toBe(randomRequestTimeout);
});

it.each([
["an options hash", { socketTimeout: randomSocketTimeout }],
[
"a provider",
async () => ({
socketTimeout: randomSocketTimeout,
}),
],
])("sets socketTimeout correctly when input is %s", async (_, option) => {
vi.spyOn(setSocketTimeoutModule, "setSocketTimeout");
const nodeHttpHandler = new NodeHttpHandler(option);
await nodeHttpHandler.handle({} as any);
expect(vi.mocked(setSocketTimeoutModule.setSocketTimeout).mock.calls[0][2]).toBe(randomRequestTimeout);
expect(vi.mocked(setSocketTimeoutModule.setSocketTimeout).mock.calls[0][2]).toBe(randomSocketTimeout);
});

it.each([
Expand Down
27 changes: 23 additions & 4 deletions packages/node-http-handler/src/node-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Agent as hsAgent, request as hsRequest } from "https";
import { NODEJS_TIMEOUT_ERROR_CODES } from "./constants";
import { getTransformedHeaders } from "./get-transformed-headers";
import { setConnectionTimeout } from "./set-connection-timeout";
import { setRequestTimeout } from "./set-request-timeout";
import { setSocketKeepAlive } from "./set-socket-keep-alive";
import { setSocketTimeout } from "./set-socket-timeout";
import { timing } from "./timing";
Expand Down Expand Up @@ -124,15 +125,24 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf
}

private resolveDefaultConfig(options?: NodeHttpHandlerOptions | void): ResolvedNodeHttpHandlerConfig {
const { requestTimeout, connectionTimeout, socketTimeout, socketAcquisitionWarningTimeout, httpAgent, httpsAgent } =
options || {};
const {
requestTimeout,
connectionTimeout,
socketTimeout,
socketAcquisitionWarningTimeout,
httpAgent,
httpsAgent,
throwOnRequestTimeout,
} = options || {};
const keepAlive = true;
const maxSockets = 50;

return {
connectionTimeout,
requestTimeout: requestTimeout ?? socketTimeout,
requestTimeout,
socketTimeout,
socketAcquisitionWarningTimeout,
throwOnRequestTimeout,
httpAgent: (() => {
if (httpAgent instanceof hAgent || typeof (httpAgent as hAgent)?.destroy === "function") {
return httpAgent as hAgent;
Expand Down Expand Up @@ -288,7 +298,16 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf
// are longer than a few seconds. This avoids slowing down faster operations.
const effectiveRequestTimeout = requestTimeout ?? this.config.requestTimeout;
timeouts.push(setConnectionTimeout(req, reject, this.config.connectionTimeout));
timeouts.push(setSocketTimeout(req, reject, effectiveRequestTimeout));
timeouts.push(
setRequestTimeout(
req,
reject,
effectiveRequestTimeout,
this.config.throwOnRequestTimeout,
this.config.logger ?? console
)
);
timeouts.push(setSocketTimeout(req, reject, this.config.socketTimeout));

// Workaround for bug report in Node.js https://github.com/nodejs/node/issues/47137
const httpAgent = nodeHttpsOptions.agent;
Expand Down
11 changes: 8 additions & 3 deletions packages/node-http-handler/src/set-connection-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,14 @@ describe("setConnectionTimeout", () => {
expect(clientRequest.destroy).toHaveBeenCalledTimes(1);
expect(reject).toHaveBeenCalledTimes(1);
expect(reject).toHaveBeenCalledWith(
Object.assign(new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`), {
name: "TimeoutError",
})
Object.assign(
new Error(
`@smithy/node-http-handler - the request socket did not establish a connection with the server within the configured timeout of ${timeoutInMs} ms.`
),
{
name: "TimeoutError",
}
)
);
});

Expand Down
11 changes: 8 additions & 3 deletions packages/node-http-handler/src/set-connection-timeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ export const setConnectionTimeout = (
const timeoutId = timing.setTimeout(() => {
request.destroy();
reject(
Object.assign(new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`), {
name: "TimeoutError",
})
Object.assign(
new Error(
`@smithy/node-http-handler - the request socket did not establish a connection with the server within the configured timeout of ${timeoutInMs} ms.`
),
{
name: "TimeoutError",
}
)
);
}, timeoutInMs - offset);

Expand Down
58 changes: 58 additions & 0 deletions packages/node-http-handler/src/set-request-timeout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, test as it, vi } from "vitest";

import { setRequestTimeout } from "./set-request-timeout";

describe("setRequestTimeout", () => {
const reject = vi.fn();
const clientRequest: any = {
destroy: vi.fn(),
};

beforeEach(() => {
vi.clearAllMocks();
});

it("returns -1 if no timeout is given", () => {
{
const id = setRequestTimeout(clientRequest, reject, 0);
expect(id).toEqual(-1);
}
{
const id = setRequestTimeout(clientRequest, reject, undefined);
expect(id).toEqual(-1);
}
});

describe("when timeout is provided", () => {
it("rejects after the timeout", async () => {
setRequestTimeout(clientRequest, reject, 1, true);
await new Promise((r) => setTimeout(r, 2));
expect(reject).toHaveBeenCalledWith(
Object.assign(
new Error(
`@smithy/node-http-handler - [ERROR] a request has exceeded the configured ${1} ms requestTimeout.`
),
{
name: "TimedoutError",
code: "ETIMEDOUT",
}
)
);
expect(clientRequest.destroy).toHaveBeenCalled();
});

it("logs a warning", async () => {
const logger = {
...console,
warn: vi.fn(),
};
setRequestTimeout(clientRequest, reject, 1, false, logger);
await new Promise((r) => setTimeout(r, 2));
expect(logger.warn).toHaveBeenCalledWith(
`@smithy/node-http-handler - [WARN] a request has exceeded the configured ${1} ms requestTimeout.` +
` Init client requestHandler with throwOnRequestTimeout=true to turn this into an error.`
);
expect(clientRequest.destroy).not.toHaveBeenCalled();
});
});
});
35 changes: 35 additions & 0 deletions packages/node-http-handler/src/set-request-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Logger } from "@smithy/types";
import type { ClientRequest } from "http";

import { timing } from "./timing";

/**
* @internal
*/
export const setRequestTimeout = (
req: ClientRequest,
reject: (err: Error) => void,
timeoutInMs = 0,
throwOnRequestTimeout?: boolean,
logger?: Logger
) => {
if (timeoutInMs) {
return timing.setTimeout(() => {
let msg = `@smithy/node-http-handler - [${
throwOnRequestTimeout ? "ERROR" : "WARN"
}] a request has exceeded the configured ${timeoutInMs} ms requestTimeout.`;
if (throwOnRequestTimeout) {
const error = Object.assign(new Error(msg), {
name: "TimedoutError",
code: "ETIMEDOUT",
});
req.destroy(error);
reject(error);
} else {
msg += ` Init client requestHandler with throwOnRequestTimeout=true to turn this into an error.`;
logger?.warn?.(msg);
}
}, timeoutInMs);
}
return -1;
};
7 changes: 6 additions & 1 deletion packages/node-http-handler/src/set-socket-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ describe("setSocketTimeout", () => {
clientRequest.setTimeout.mock.calls[0][1]();
expect(reject).toHaveBeenCalledTimes(1);
expect(reject).toHaveBeenCalledWith(
Object.assign(new Error(`Connection timed out after ${timeoutInMs} ms`), { name: "TimeoutError" })
Object.assign(
new Error(
`@smithy/node-http-handler - the request socket timed out after ${timeoutInMs} ms of inactivity (configured by client requestHandler).`
),
{ name: "TimeoutError" }
)
);
});
});
12 changes: 9 additions & 3 deletions packages/node-http-handler/src/set-socket-timeout.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import type { ClientRequest } from "http";

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

const DEFER_EVENT_LISTENER_TIME = 3000;

export const setSocketTimeout = (
request: ClientRequest,
reject: (err: Error) => void,
timeoutInMs = DEFAULT_REQUEST_TIMEOUT
timeoutInMs = 0
): NodeJS.Timeout | number => {
const registerTimeout = (offset: number) => {
const timeout = timeoutInMs - offset;
const onTimeout = () => {
request.destroy();
reject(Object.assign(new Error(`Connection timed out after ${timeoutInMs} ms`), { name: "TimeoutError" }));
reject(
Object.assign(
new Error(
`@smithy/node-http-handler - the request socket timed out after ${timeoutInMs} ms of inactivity (configured by client requestHandler).`
),
{ name: "TimeoutError" }
)
);
};

if (request.socket) {
Expand Down
30 changes: 19 additions & 11 deletions packages/types/src/http/httpHandlerInitialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,43 @@ export interface NodeHttpHandlerOptions {
/**
* The maximum time in milliseconds that the connection phase of a request
* may take before the connection attempt is abandoned.
*
* Defaults to 0, which disables the timeout.
*/
connectionTimeout?: number;

/**
* The number of milliseconds a request can take before automatically being terminated.
* The maximum number of milliseconds request & response should take.
* Defaults to 0, which disables the timeout.
*
* If exceeded, a warning will be emitted unless throwOnRequestTimeout=true,
* in which case a TimeoutError will be thrown.
*/
requestTimeout?: number;

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

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

/**
* Delay before the NodeHttpHandler checks for socket exhaustion,
* and emits a warning if the active sockets and enqueued request count is greater than
* 2x the maxSockets count.
*
* Defaults to connectionTimeout + requestTimeout or 3000ms if those are not set.
*/
socketAcquisitionWarningTimeout?: number;

/**
* You can pass http.Agent or its constructor options.
*/
Expand Down