@gentrace/core
Version:
Core Gentrace Node.JS library
760 lines (661 loc) • 24.7 kB
JavaScript
;
import utils from "../utils.js";
import settle from "../core/settle.js";
import buildFullPath from "../core/buildFullPath.js";
import buildURL from "../helpers/buildURL.js";
import { getProxyForUrl } from "proxy-from-env";
import http from "http";
import https from "https";
import util from "util";
import followRedirects from "follow-redirects";
import zlib from "zlib";
import { VERSION } from "../env/data.js";
import transitionalDefaults from "../defaults/transitional.js";
import AxiosError from "../core/AxiosError.js";
import CanceledError from "../cancel/CanceledError.js";
import platform from "../platform/index.js";
import fromDataURI from "../helpers/fromDataURI.js";
import stream from "stream";
import AxiosHeaders from "../core/AxiosHeaders.js";
import AxiosTransformStream from "../helpers/AxiosTransformStream.js";
import EventEmitter from "events";
import formDataToStream from "../helpers/formDataToStream.js";
import readBlob from "../helpers/readBlob.js";
import ZlibHeaderTransformStream from "../helpers/ZlibHeaderTransformStream.js";
import callbackify from "../helpers/callbackify.js";
const zlibOptions = {
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH,
};
const brotliOptions = {
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH,
};
const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);
const { http: httpFollow, https: httpsFollow } = followRedirects;
const isHttps = /https:?/;
const supportedProtocols = platform.protocols.map((protocol) => {
return protocol + ":";
});
/**
* If the proxy or config beforeRedirects functions are defined, call them with the options
* object.
*
* @param {Object<string, any>} options - The options object that was passed to the request.
*
* @returns {Object<string, any>}
*/
function dispatchBeforeRedirect(options) {
if (options.beforeRedirects.proxy) {
options.beforeRedirects.proxy(options);
}
if (options.beforeRedirects.config) {
options.beforeRedirects.config(options);
}
}
/**
* If the proxy or config afterRedirects functions are defined, call them with the options
*
* @param {http.ClientRequestArgs} options
* @param {AxiosProxyConfig} configProxy configuration from Axios options object
* @param {string} location
*
* @returns {http.ClientRequestArgs}
*/
function setProxy(options, configProxy, location) {
let proxy = configProxy;
if (!proxy && proxy !== false) {
const proxyUrl = getProxyForUrl(location);
if (proxyUrl) {
proxy = new URL(proxyUrl);
}
}
if (proxy) {
// Basic proxy authorization
if (proxy.username) {
proxy.auth = (proxy.username || "") + ":" + (proxy.password || "");
}
if (proxy.auth) {
// Support proxy auth object form
if (proxy.auth.username || proxy.auth.password) {
proxy.auth =
(proxy.auth.username || "") + ":" + (proxy.auth.password || "");
}
const base64 = Buffer.from(proxy.auth, "utf8").toString("base64");
options.headers["Proxy-Authorization"] = "Basic " + base64;
}
options.headers.host =
options.hostname + (options.port ? ":" + options.port : "");
const proxyHost = proxy.hostname || proxy.host;
options.hostname = proxyHost;
// Replace 'host' since options is not a URL object
options.host = proxyHost;
options.port = proxy.port;
options.path = location;
if (proxy.protocol) {
options.protocol = proxy.protocol.includes(":")
? proxy.protocol
: `${proxy.protocol}:`;
}
}
options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) {
// Configure proxy for redirected request, passing the original config proxy to apply
// the exact same logic as if the redirected request was performed by axios directly.
setProxy(redirectOptions, configProxy, redirectOptions.href);
};
}
const isHttpAdapterSupported =
typeof process !== "undefined" && utils.kindOf(process) === "process";
// temporary hotfix
const wrapAsync = (asyncExecutor) => {
return new Promise((resolve, reject) => {
let onDone;
let isDone;
const done = (value, isRejected) => {
if (isDone) return;
isDone = true;
onDone && onDone(value, isRejected);
};
const _resolve = (value) => {
done(value);
resolve(value);
};
const _reject = (reason) => {
done(reason, true);
reject(reason);
};
asyncExecutor(
_resolve,
_reject,
(onDoneHandler) => (onDone = onDoneHandler)
).catch(_reject);
});
};
/*eslint consistent-return:0*/
export default isHttpAdapterSupported &&
function httpAdapter(config) {
return wrapAsync(
async function dispatchHttpRequest(resolve, reject, onDone) {
let { data, lookup, family } = config;
const { responseType, responseEncoding } = config;
const method = config.method.toUpperCase();
let isDone;
let rejected = false;
let req;
if (lookup && utils.isAsyncFn(lookup)) {
lookup = callbackify(lookup, (entry) => {
if (utils.isString(entry)) {
entry = [entry, entry.indexOf(".") < 0 ? 6 : 4];
} else if (!utils.isArray(entry)) {
throw new TypeError(
"lookup async function must return an array [ip: string, family: number]]"
);
}
return entry;
});
}
// temporary internal emitter until the AxiosRequest class will be implemented
const emitter = new EventEmitter();
const onFinished = () => {
if (config.cancelToken) {
config.cancelToken.unsubscribe(abort);
}
if (config.signal) {
config.signal.removeEventListener("abort", abort);
}
emitter.removeAllListeners();
};
onDone((value, isRejected) => {
isDone = true;
if (isRejected) {
rejected = true;
onFinished();
}
});
function abort(reason) {
emitter.emit(
"abort",
!reason || reason.type
? new CanceledError(null, config, req)
: reason
);
}
emitter.once("abort", reject);
if (config.cancelToken || config.signal) {
config.cancelToken && config.cancelToken.subscribe(abort);
if (config.signal) {
config.signal.aborted
? abort()
: config.signal.addEventListener("abort", abort);
}
}
// Parse url
const fullPath = buildFullPath(config.baseURL, config.url);
const parsed = new URL(fullPath, "http://localhost");
const protocol = parsed.protocol || supportedProtocols[0];
if (protocol === "data:") {
let convertedData;
if (method !== "GET") {
return settle(resolve, reject, {
status: 405,
statusText: "method not allowed",
headers: {},
config,
});
}
try {
convertedData = fromDataURI(config.url, responseType === "blob", {
Blob: config.env && config.env.Blob,
});
} catch (err) {
throw AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config);
}
if (responseType === "text") {
convertedData = convertedData.toString(responseEncoding);
if (!responseEncoding || responseEncoding === "utf8") {
convertedData = utils.stripBOM(convertedData);
}
} else if (responseType === "stream") {
convertedData = stream.Readable.from(convertedData);
}
return settle(resolve, reject, {
data: convertedData,
status: 200,
statusText: "OK",
headers: new AxiosHeaders(),
config,
});
}
if (supportedProtocols.indexOf(protocol) === -1) {
return reject(
new AxiosError(
"Unsupported protocol " + protocol,
AxiosError.ERR_BAD_REQUEST,
config
)
);
}
const headers = AxiosHeaders.from(config.headers).normalize();
// Set User-Agent (required by some servers)
// See https://github.com/axios/axios/issues/69
// User-Agent is specified; handle case where no UA header is desired
// Only set header if it hasn't been set in config
headers.set("User-Agent", "axios/" + VERSION, false);
const onDownloadProgress = config.onDownloadProgress;
const onUploadProgress = config.onUploadProgress;
const maxRate = config.maxRate;
let maxUploadRate = undefined;
let maxDownloadRate = undefined;
// support for spec compliant FormData objects
if (utils.isSpecCompliantForm(data)) {
const userBoundary = headers.getContentType(
/boundary=([-_\w\d]{10,70})/i
);
data = formDataToStream(
data,
(formHeaders) => {
headers.set(formHeaders);
},
{
tag: `axios-${VERSION}-boundary`,
boundary: (userBoundary && userBoundary[1]) || undefined,
}
);
// support for https://www.npmjs.com/package/form-data api
} else if (
utils.isFormData(data) &&
utils.isFunction(data.getHeaders)
) {
headers.set(data.getHeaders());
if (!headers.hasContentLength()) {
try {
const knownLength = await util
.promisify(data.getLength)
.call(data);
Number.isFinite(knownLength) &&
knownLength >= 0 &&
headers.setContentLength(knownLength);
/*eslint no-empty:0*/
} catch (e) {}
}
} else if (utils.isBlob(data)) {
data.size &&
headers.setContentType(data.type || "application/octet-stream");
headers.setContentLength(data.size || 0);
data = stream.Readable.from(readBlob(data));
} else if (data && !utils.isStream(data)) {
if (Buffer.isBuffer(data)) {
// Nothing to do...
} else if (utils.isArrayBuffer(data)) {
data = Buffer.from(new Uint8Array(data));
} else if (utils.isString(data)) {
data = Buffer.from(data, "utf-8");
} else {
return reject(
new AxiosError(
"Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream",
AxiosError.ERR_BAD_REQUEST,
config
)
);
}
// Add Content-Length header if data exists
headers.setContentLength(data.length, false);
if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) {
return reject(
new AxiosError(
"Request body larger than maxBodyLength limit",
AxiosError.ERR_BAD_REQUEST,
config
)
);
}
}
const contentLength = utils.toFiniteNumber(headers.getContentLength());
if (utils.isArray(maxRate)) {
maxUploadRate = maxRate[0];
maxDownloadRate = maxRate[1];
} else {
maxUploadRate = maxDownloadRate = maxRate;
}
if (data && (onUploadProgress || maxUploadRate)) {
if (!utils.isStream(data)) {
data = stream.Readable.from(data, { objectMode: false });
}
data = stream.pipeline(
[
data,
new AxiosTransformStream({
length: contentLength,
maxRate: utils.toFiniteNumber(maxUploadRate),
}),
],
utils.noop
);
onUploadProgress &&
data.on("progress", (progress) => {
onUploadProgress(
Object.assign(progress, {
upload: true,
})
);
});
}
// HTTP basic authentication
let auth = undefined;
if (config.auth) {
const username = config.auth.username || "";
const password = config.auth.password || "";
auth = username + ":" + password;
}
if (!auth && parsed.username) {
const urlUsername = parsed.username;
const urlPassword = parsed.password;
auth = urlUsername + ":" + urlPassword;
}
auth && headers.delete("authorization");
let path;
try {
path = buildURL(
parsed.pathname + parsed.search,
config.params,
config.paramsSerializer
).replace(/^\?/, "");
} catch (err) {
const customErr = new Error(err.message);
customErr.config = config;
customErr.url = config.url;
customErr.exists = true;
return reject(customErr);
}
headers.set(
"Accept-Encoding",
"gzip, compress, deflate" + (isBrotliSupported ? ", br" : ""),
false
);
const options = {
path,
method: method,
headers: headers.toJSON(),
agents: { http: config.httpAgent, https: config.httpsAgent },
auth,
protocol,
family,
lookup,
beforeRedirect: dispatchBeforeRedirect,
beforeRedirects: {},
};
if (config.socketPath) {
options.socketPath = config.socketPath;
} else {
options.hostname = parsed.hostname;
options.port = parsed.port;
setProxy(
options,
config.proxy,
protocol +
"//" +
parsed.hostname +
(parsed.port ? ":" + parsed.port : "") +
options.path
);
}
let transport;
const isHttpsRequest = isHttps.test(options.protocol);
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
if (config.transport) {
transport = config.transport;
} else if (config.maxRedirects === 0) {
transport = isHttpsRequest ? https : http;
} else {
if (config.maxRedirects) {
options.maxRedirects = config.maxRedirects;
}
if (config.beforeRedirect) {
options.beforeRedirects.config = config.beforeRedirect;
}
transport = isHttpsRequest ? httpsFollow : httpFollow;
}
if (config.maxBodyLength > -1) {
options.maxBodyLength = config.maxBodyLength;
} else {
// follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited
options.maxBodyLength = Infinity;
}
if (config.insecureHTTPParser) {
options.insecureHTTPParser = config.insecureHTTPParser;
}
// Create the request
req = transport.request(options, function handleResponse(res) {
if (req.destroyed) return;
const streams = [res];
const responseLength = +res.headers["content-length"];
if (onDownloadProgress) {
const transformStream = new AxiosTransformStream({
length: utils.toFiniteNumber(responseLength),
maxRate: utils.toFiniteNumber(maxDownloadRate),
});
onDownloadProgress &&
transformStream.on("progress", (progress) => {
onDownloadProgress(
Object.assign(progress, {
download: true,
})
);
});
streams.push(transformStream);
}
// decompress the response body transparently if required
let responseStream = res;
// return the last request in case of redirects
const lastRequest = res.req || req;
// if decompress disabled we should not decompress
if (config.decompress !== false && res.headers["content-encoding"]) {
// if no content, but headers still say that it is encoded,
// remove the header not confuse downstream operations
if (method === "HEAD" || res.statusCode === 204) {
delete res.headers["content-encoding"];
}
switch (res.headers["content-encoding"]) {
/*eslint default-case:0*/
case "gzip":
case "x-gzip":
case "compress":
case "x-compress":
// add the unzipper to the body stream processing pipeline
streams.push(zlib.createUnzip(zlibOptions));
// remove the content-encoding in order to not confuse downstream operations
delete res.headers["content-encoding"];
break;
case "deflate":
streams.push(new ZlibHeaderTransformStream());
// add the unzipper to the body stream processing pipeline
streams.push(zlib.createUnzip(zlibOptions));
// remove the content-encoding in order to not confuse downstream operations
delete res.headers["content-encoding"];
break;
case "br":
if (isBrotliSupported) {
streams.push(zlib.createBrotliDecompress(brotliOptions));
delete res.headers["content-encoding"];
}
}
}
responseStream =
streams.length > 1
? stream.pipeline(streams, utils.noop)
: streams[0];
const offListeners = stream.finished(responseStream, () => {
offListeners();
onFinished();
});
const response = {
status: res.statusCode,
statusText: res.statusMessage,
headers: new AxiosHeaders(res.headers),
config,
request: lastRequest,
};
if (responseType === "stream") {
response.data = responseStream;
settle(resolve, reject, response);
} else {
const responseBuffer = [];
let totalResponseBytes = 0;
responseStream.on("data", function handleStreamData(chunk) {
responseBuffer.push(chunk);
totalResponseBytes += chunk.length;
// make sure the content length is not over the maxContentLength if specified
if (
config.maxContentLength > -1 &&
totalResponseBytes > config.maxContentLength
) {
// stream.destroy() emit aborted event before calling reject() on Node.js v16
rejected = true;
responseStream.destroy();
reject(
new AxiosError(
"maxContentLength size of " +
config.maxContentLength +
" exceeded",
AxiosError.ERR_BAD_RESPONSE,
config,
lastRequest
)
);
}
});
responseStream.on("aborted", function handlerStreamAborted() {
if (rejected) {
return;
}
const err = new AxiosError(
"maxContentLength size of " +
config.maxContentLength +
" exceeded",
AxiosError.ERR_BAD_RESPONSE,
config,
lastRequest
);
responseStream.destroy(err);
reject(err);
});
responseStream.on("error", function handleStreamError(err) {
if (req.destroyed) return;
reject(AxiosError.from(err, null, config, lastRequest));
});
responseStream.on("end", function handleStreamEnd() {
try {
let responseData =
responseBuffer.length === 1
? responseBuffer[0]
: Buffer.concat(responseBuffer);
if (responseType !== "arraybuffer") {
responseData = responseData.toString(responseEncoding);
if (!responseEncoding || responseEncoding === "utf8") {
responseData = utils.stripBOM(responseData);
}
}
response.data = responseData;
} catch (err) {
reject(
AxiosError.from(err, null, config, response.request, response)
);
}
settle(resolve, reject, response);
});
}
emitter.once("abort", (err) => {
if (!responseStream.destroyed) {
responseStream.emit("error", err);
responseStream.destroy();
}
});
});
emitter.once("abort", (err) => {
reject(err);
req.destroy(err);
});
// Handle errors
req.on("error", function handleRequestError(err) {
// @todo remove
// if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return;
reject(AxiosError.from(err, null, config, req));
});
// set tcp keep alive to prevent drop connection by peer
req.on("socket", function handleRequestSocket(socket) {
// default interval of sending ack packet is 1 minute
socket.setKeepAlive(true, 1000 * 60);
});
// Handle request timeout
if (config.timeout) {
// This is forcing a int timeout to avoid problems if the `req` interface doesn't handle other types.
const timeout = parseInt(config.timeout, 10);
if (isNaN(timeout)) {
reject(
new AxiosError(
"error trying to parse `config.timeout` to int",
AxiosError.ERR_BAD_OPTION_VALUE,
config,
req
)
);
return;
}
// Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
// And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
// At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
// And then these socket which be hang up will devouring CPU little by little.
// ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
req.setTimeout(timeout, function handleRequestTimeout() {
if (isDone) return;
let timeoutErrorMessage = config.timeout
? "timeout of " + config.timeout + "ms exceeded"
: "timeout exceeded";
const transitional = config.transitional || transitionalDefaults;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(
new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError
? AxiosError.ETIMEDOUT
: AxiosError.ECONNABORTED,
config,
req
)
);
abort();
});
}
// Send the request
if (utils.isStream(data)) {
let ended = false;
let errored = false;
data.on("end", () => {
ended = true;
});
data.once("error", (err) => {
errored = true;
req.destroy(err);
});
data.on("close", () => {
if (!ended && !errored) {
abort(
new CanceledError(
"Request stream has been aborted",
config,
req
)
);
}
});
data.pipe(req);
} else {
req.end(data);
}
}
);
};
export const __setProxy = setProxy;