UNPKG

serverless-spy

Version:

CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.

209 lines (208 loc) 8.87 kB
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 { timing } from "./timing"; 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, logger = console) { 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) { logger?.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 }); })(), logger: console, }; } destroy() { this.config?.httpAgent?.destroy(); this.config?.httpsAgent?.destroy(); } async handle(request, { abortSignal } = {}) { if (!this.config) { this.config = await this.configProvider; } return new Promise((_resolve, _reject) => { let writeRequestBodyPromise = undefined; const timeouts = []; const resolve = async (arg) => { await writeRequestBodyPromise; timeouts.forEach(timing.clearTimeout); _resolve(arg); }; const reject = async (arg) => { await writeRequestBodyPromise; timeouts.forEach(timing.clearTimeout); _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; timeouts.push(timing.setTimeout(() => { this.socketWarningTimestamp = NodeHttpHandler.checkSocketUsage(agent, this.socketWarningTimestamp, this.config.logger); }, 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}`; } let hostname = request.hostname ?? ""; if (hostname[0] === "[" && hostname.endsWith("]")) { hostname = request.hostname.slice(1, -1); } else { hostname = request.hostname; } const nodeHttpsOptions = { headers: request.headers, host: 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); } }); if (abortSignal) { const onAbort = () => { req.destroy(); const abortError = new Error("Request aborted"); abortError.name = "AbortError"; reject(abortError); }; if (typeof abortSignal.addEventListener === "function") { const signal = abortSignal; signal.addEventListener("abort", onAbort, { once: true }); req.once("close", () => signal.removeEventListener("abort", onAbort)); } else { abortSignal.onabort = onAbort; } } timeouts.push(setConnectionTimeout(req, reject, this.config.connectionTimeout)); timeouts.push(setSocketTimeout(req, reject, this.config.requestTimeout)); const httpAgent = nodeHttpsOptions.agent; if (typeof httpAgent === "object" && "keepAlive" in httpAgent) { timeouts.push(setSocketKeepAlive(req, { keepAlive: httpAgent.keepAlive, keepAliveMsecs: httpAgent.keepAliveMsecs, })); } writeRequestBodyPromise = writeRequestBody(req, request, this.config.requestTimeout).catch((e) => { timeouts.forEach(timing.clearTimeout); return _reject(e); }); }); } updateHttpClientConfig(key, value) { this.config = undefined; this.configProvider = this.configProvider.then((config) => { return { ...config, [key]: value, }; }); } httpHandlerConfigs() { return this.config ?? {}; } }