@freemework/common
Version:
Common library of the Freemework Project.
274 lines • 13.8 kB
JavaScript
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