UNPKG

@freemework/common

Version:

Common library of the Freemework Project.

274 lines 13.8 kB
import * as http from "http"; import * as https from "https"; import { parse as contentTypeParse, } from "content-type"; import { FCancellationExecutionContext, FCancellationException } from "../cancellation/index.js"; import { FException, FExceptionInvalidOperation, FExceptionNativeErrorWrapper } from "../exception/index.js"; import { FLogger, FLoggerLabel, FLoggerLabelsExecutionContext, FLoggerLevel } from "../logging/index.js"; export class FHttpClientLoggerLabel extends FLoggerLabel { static URL = new FHttpClientLoggerLabel("out.http.url", "Describes HTTP URL of output request"); static METHOD = new FHttpClientLoggerLabel("out.http.method", "Describes HTTP method of output request (like a GET, POST, etc.)"); } export class FHttpClient { _log; _logLevelHeaders; _logLevelBody; _proxyOpts; _sslOpts; _requestTimeout; constructor(opts) { this._log = opts !== undefined && opts.log !== undefined ? opts.log : FLogger.create(this.constructor.name); this._logLevelBody = opts !== undefined && opts.logLevelBody !== undefined ? opts.logLevelBody : FLoggerLevel.DEBUG; this._logLevelHeaders = opts !== undefined && opts.logLevelHeaders !== undefined ? opts.logLevelHeaders : FLoggerLevel.DEBUG; this._proxyOpts = opts !== undefined && opts.proxyOpts !== undefined ? opts.proxyOpts : null; this._sslOpts = opts !== undefined && opts.sslOpts !== undefined ? opts.sslOpts : null; this._requestTimeout = opts !== undefined && opts.timeout !== undefined ? opts.timeout : FHttpClient.DEFAULT_TIMEOUT; } async invoke(executionContext, { url, method, headers, body }) { executionContext = new FLoggerLabelsExecutionContext(executionContext, FHttpClientLoggerLabel.METHOD.value(method), FHttpClientLoggerLabel.URL.value(url.toString())); this._log.trace(executionContext, "Begin invoke"); try { return await new Promise((resolve, reject) => { const cancellationToken = FCancellationExecutionContext.of(executionContext).cancellationToken; let isConnectTimeout = false; let resolved = false; const errorHandler = (e) => { const ex = FException.wrapIfNeeded(e); if (!resolved) { resolved = true; const msg = isConnectTimeout ? "Connect Timeout" : `${method} ${url} failed with error: ${ex.message}. See innerException for details`; this._log.debug(executionContext, msg, ex); return reject(new FHttpClient.CommunicationError(url, method, headers !== undefined ? headers : {}, body !== undefined ? body : Buffer.alloc(0), msg, ex)); } }; const responseHandler = (response) => { const responseDataChunks = []; response.on("data", (chunk) => responseDataChunks.push(chunk)); response.on("error", errorHandler); response.on("end", () => { if (!resolved) { resolved = true; if (isConnectTimeout) { return reject(new FHttpClient.CommunicationError(url, method, headers !== undefined ? headers : {}, body !== undefined ? body : Buffer.alloc(0), "Connect Timeout")); } const respStatus = response.statusCode || 500; const respDescription = response.statusMessage || ""; const respHeaders = response.headers; const respBody = Buffer.concat(responseDataChunks); this._log.log(executionContext, this._logLevelHeaders, () => `Recv head: ${JSON.stringify({ respStatus, respDescription, respHeaders })}`); this._log.log(executionContext, this._logLevelBody, () => `Recv body: ${respBody.toString()}`); if (respStatus < 400) { return resolve({ statusCode: respStatus, statusDescription: respDescription, headers: respHeaders, body: respBody, }); } else { return reject(new FHttpClient.WebError(respStatus, respDescription, url, method, headers !== undefined ? headers : {}, body !== undefined ? body : Buffer.alloc(0), respHeaders, respBody)); } } }); }; try { cancellationToken.throwIfCancellationRequested(); // Will raise error, we want to reject this Promise exactly with cancellation exception. } catch (e) { return reject(e); } this._log.log(executionContext, this._logLevelHeaders, () => `Write head: ${JSON.stringify({ reqHeaders: headers })}`); const request = this.createClientRequest(executionContext, { url, method, headers: headers }, responseHandler); if (body !== undefined) { this._log.log(executionContext, this._logLevelBody, () => `Write body: ${body.toString()}`); request.write(body); } request.end(); request.on("error", errorHandler); request.on("close", () => request.removeListener("close", errorHandler)); // Prevent memory-leaks request.setTimeout(this._requestTimeout, () => { isConnectTimeout = true; request.destroy(); }); request.on("socket", socket => { // this will setup connect timeout socket.setTimeout(this._requestTimeout); // socket.on("timeout", () => { // isConnectTimeout = true; // request.abort(); // }); }); if (cancellationToken !== undefined) { const cb = () => { request.destroy(); if (!resolved) { resolved = true; try { cancellationToken.throwIfCancellationRequested(); // Should raise error // Guard for broken implementation of cancellationToken reject(new FCancellationException("Cancelled by user")); } catch (e) { reject(e); } } }; cancellationToken.addCancelListener(cb); request.on("close", () => cancellationToken.removeCancelListener(cb)); // Prevent memory-leaks } }); } finally { this._log.trace(executionContext, "End invoke"); } } createClientRequest(executionContext, { url, method, headers }, callback) { const proxyOpts = this._proxyOpts; if (proxyOpts && proxyOpts.type === "http") { const reqOpts = { protocol: "http:", host: proxyOpts.host, port: proxyOpts.port, path: url.href, method, headers: { Host: url.host, ...headers } }; this._log.trace(executionContext, () => `http.request(${JSON.stringify(reqOpts)})`); return http.request(reqOpts, callback); } else { const reqOpts = { protocol: url.protocol, host: url.hostname, port: url.port, path: url.pathname + url.search, method: method, headers: headers }; if (reqOpts.protocol === "https:") { const sslOpts = this._sslOpts; if (sslOpts) { if (sslOpts.ca) { reqOpts.ca = sslOpts.ca; } if (sslOpts.rejectUnauthorized !== undefined) { reqOpts.rejectUnauthorized = sslOpts.rejectUnauthorized; } if ("pfx" in sslOpts) { reqOpts.pfx = sslOpts.pfx; reqOpts.passphrase = sslOpts.passphrase; } else if ("cert" in sslOpts) { reqOpts.key = sslOpts.key; reqOpts.cert = sslOpts.cert; } } this._log.trace(executionContext, () => `https.request(${JSON.stringify(reqOpts)})`); return https.request(reqOpts, callback); } else { this._log.trace(executionContext, () => `http.request(${JSON.stringify(reqOpts)})`); return http.request(reqOpts, callback); } } } } (function (FHttpClient) { FHttpClient.DEFAULT_TIMEOUT = 60000; let HttpMethod; (function (HttpMethod) { HttpMethod["CONNECT"] = "CONNECT"; HttpMethod["DELETE"] = "DELETE"; HttpMethod["HEAD"] = "HEAD"; HttpMethod["GET"] = "GET"; HttpMethod["OPTIONS"] = "OPTIONS"; HttpMethod["PATCH"] = "PATCH"; HttpMethod["POST"] = "POST"; HttpMethod["PUT"] = "PUT"; HttpMethod["TRACE"] = "TRACE"; })(HttpMethod = FHttpClient.HttpMethod || (FHttpClient.HttpMethod = {})); /** Base error type for WebClient */ class HttpClientError extends FException { _url; _method; _requestHeaders; _requestBody; constructor(url, method, requestHeaders, requestBody, errorMessage, innerException) { super(errorMessage, innerException); this._url = url; this._method = method; this._requestHeaders = requestHeaders; this._requestBody = requestBody; } get url() { return this._url; } get method() { return this._method; } get requestHeaders() { return this._requestHeaders; } get requestBody() { return this._requestBody; } get requestObject() { return parseJsonBody(this.requestBody, this.requestHeaders); } } FHttpClient.HttpClientError = HttpClientError; /** * WebError is a wrapper of HTTP responses with code 4xx/5xx */ class WebError extends HttpClientError { _statusCode; _statusDescription; _responseHeaders; _responseBody; constructor(statusCode, statusDescription, url, method, requestHeaders, requestBody, responseHeaders, responseBody, innerException) { super(url, method, requestHeaders, requestBody, `${statusCode} ${statusDescription}`, innerException); this._statusCode = statusCode; this._statusDescription = statusDescription; this._responseHeaders = responseHeaders; this._responseBody = responseBody; } get statusCode() { return this._statusCode; } get statusDescription() { return this._statusDescription; } get headers() { return this._responseHeaders; } get body() { return this._responseBody; } get object() { return parseJsonBody(this.body, this.headers); } } FHttpClient.WebError = WebError; /** * CommunicationError is a wrapper over underlying network errors. * Such a DNS lookup issues, TCP connection issues, etc... */ class CommunicationError extends HttpClientError { constructor(url, method, requestHeaders, requestBody, errorMessage, innerException) { super(url, method, requestHeaders, requestBody, errorMessage, innerException); } get code() { const innerException = this.innerException; if (innerException !== null && innerException instanceof FExceptionNativeErrorWrapper) { const error = innerException.nativeError; if ("code" in error) { const code = error.code; if (typeof (code) === "string") { return code; } } } return null; } } FHttpClient.CommunicationError = CommunicationError; })(FHttpClient || (FHttpClient = {})); function parseJsonBody(body, headers) { let contentType = null; if (headers !== null) { const contentTypeHeaderName = Object.keys(headers).find(header => header.toLowerCase() === "content-type"); if (contentTypeHeaderName !== undefined) { const headerValue = headers[contentTypeHeaderName]; if (typeof headerValue === "string") { contentType = contentTypeParse(headerValue); } } } if (contentType === null || contentType.type !== "application/json" || ("charset" in contentType.parameters && contentType.parameters["charset"].toLowerCase() !== "utf-8")) { throw new FExceptionInvalidOperation("Wrong operation. The property available only for UTF-8 'application/json' content type."); } const friendlyBody = body instanceof Buffer ? body : Buffer.from(body); return JSON.parse(friendlyBody.toString("utf-8")); } //# sourceMappingURL=f_http_client.js.map