UNPKG

@gentrace/core

Version:
760 lines (661 loc) 24.7 kB
"use strict"; 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;