import { HttpResponse } from "@smithy/protocol-http"; import { buildQueryString } from "@smithy/querystring-builder"; import { Agent as hAgent, request as hRequest } from "http"; 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 { setSocketKeepAlive } from "./set-socket-keep-alive"; import { setSocketTimeout } from "./set-socket-timeout"; import { writeRequestBody } from "./write-request-body"; export const DEFAULT_REQUEST_TIMEOUT = 0; export class NodeHttpHandler { static create(instanceOrOptions) { if (typeof instanceOrOptions?.handle === "function") { return instanceOrOptions; } return new NodeHttpHandler(instanceOrOptions); } static checkSocketUsage(agent, socketWarningTimestamp) { const { sockets, requests, maxSockets } = agent; if (typeof maxSockets !== "number" || maxSockets === Infinity) { return socketWarningTimestamp; } const interval = 15000; if (Date.now() - interval < socketWarningTimestamp) { return socketWarningTimestamp; } if (sockets && requests) { for (const origin in sockets) { const socketsInUse = sockets[origin]?.length ?? 0; const requestsEnqueued = requests[origin]?.length ?? 0; if (socketsInUse >= maxSockets && requestsEnqueued >= 2 * maxSockets) { console.warn("@smithy/node-http-handler:WARN", `socket usage at capacity=${socketsInUse} and ${requestsEnqueued} additional requests are enqueued.`, "See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html", "or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler config."); return Date.now(); } } } return socketWarningTimestamp; } constructor(options) { this.socketWarningTimestamp = 0; this.metadata = { handlerProtocol: "http/1.1" }; this.configProvider = new Promise((resolve, reject) => { if (typeof options === "function") { options() .then((_options) => { resolve(this.resolveDefaultConfig(_options)); }) .catch(reject); } else { resolve(this.resolveDefaultConfig(options)); } }); } resolveDefaultConfig(options) { const { requestTimeout, connectionTimeout, socketTimeout, httpAgent, httpsAgent } = options || {}; const keepAlive = true; const maxSockets = 50; return { connectionTimeout, requestTimeout: requestTimeout ?? socketTimeout, httpAgent: (() => { if (httpAgent instanceof hAgent || typeof httpAgent?.destroy === "function") { return httpAgent; } return new hAgent({ keepAlive, maxSockets, ...httpAgent }); })(), httpsAgent: (() => { if (httpsAgent instanceof hsAgent || typeof httpsAgent?.destroy === "function") { return httpsAgent; } return new hsAgent({ keepAlive, maxSockets, ...httpsAgent }); })(), }; } destroy() { this.config?.httpAgent?.destroy(); this.config?.httpsAgent?.destroy(); } async handle(request, { abortSignal } = {}) { if (!this.config) { this.config = await this.configProvider; } let socketCheckTimeoutId; return new Promise((_resolve, _reject) => { let writeRequestBodyPromise = undefined; const resolve = async (arg) => { await writeRequestBodyPromise; clearTimeout(socketCheckTimeoutId); _resolve(arg); }; const reject = async (arg) => { await writeRequestBodyPromise; _reject(arg); }; if (!this.config) { throw new Error("Node HTTP request handler config is not resolved"); } if (abortSignal?.aborted) { const abortError = new Error("Request aborted"); abortError.name = "AbortError"; reject(abortError); return; } const isSSL = request.protocol === "https:"; const agent = isSSL ? this.config.httpsAgent : this.config.httpAgent; socketCheckTimeoutId = setTimeout(() => { this.socketWarningTimestamp = NodeHttpHandler.checkSocketUsage(agent, this.socketWarningTimestamp); }, this.config.socketAcquisitionWarningTimeout ?? (this.config.requestTimeout ?? 2000) + (this.config.connectionTimeout ?? 1000)); const queryString = buildQueryString(request.query || {}); let auth = undefined; if (request.username != null || request.password != null) { const username = request.username ?? ""; const password = request.password ?? ""; auth = `${username}:${password}`; } let path = request.path; if (queryString) { path += `?${queryString}`; } if (request.fragment) { path += `#${request.fragment}`; } const nodeHttpsOptions = { headers: request.headers, host: request.hostname, method: request.method, path, port: request.port, agent, auth, }; const requestFunc = isSSL ? hsRequest : hRequest; const req = requestFunc(nodeHttpsOptions, (res) => { const httpResponse = new HttpResponse({ statusCode: res.statusCode || -1, reason: res.statusMessage, headers: getTransformedHeaders(res.headers), body: res, }); resolve({ response: httpResponse }); }); req.on("error", (err) => { if (NODEJS_TIMEOUT_ERROR_CODES.includes(err.code)) { reject(Object.assign(err, { name: "TimeoutError" })); } else { reject(err); } }); setConnectionTimeout(req, reject, this.config.connectionTimeout); setSocketTimeout(req, reject, this.config.requestTimeout); if (abortSignal) { abortSignal.onabort = () => { req.abort(); const abortError = new Error("Request aborted"); abortError.name = "AbortError"; reject(abortError); }; } const httpAgent = nodeHttpsOptions.agent; if (typeof httpAgent === "object" && "keepAlive" in httpAgent) { setSocketKeepAlive(req, { keepAlive: httpAgent.keepAlive, keepAliveMsecs: httpAgent.keepAliveMsecs, }); } writeRequestBodyPromise = writeRequestBody(req, request, this.config.requestTimeout).catch(_reject); }); } updateHttpClientConfig(key, value) { this.config = undefined; this.configProvider = this.configProvider.then((config) => { return { ...config, [key]: value, }; }); } httpHandlerConfigs() { return this.config ?? {}; } }