|
|
'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 proxyFromEnv 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"; import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../helpers/progressEventReducer.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 + ':'; });
const flushOnFinish = (stream, [throttled, flush]) => { stream .on('end', flush) .on('error', flush);
return throttled; }
/** * 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, responseDetails) { if (options.beforeRedirects.proxy) { options.beforeRedirects.proxy(options); } if (options.beforeRedirects.config) { options.beforeRedirects.config(options, responseDetails); } }
/** * 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 = proxyFromEnv.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); }) };
const resolveFamily = ({address, family}) => { if (!utils.isString(address)) { throw TypeError('address must be a string'); } return ({ address, family: family || (address.indexOf('.') < 0 ? 6 : 4) }); }
const buildAddressEntry = (address, family) => resolveFamily(utils.isObject(address) ? address : {address, family});
/*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) { const _lookup = callbackify(lookup, (value) => utils.isArray(value) ? value : [value]); // hotfix to support opt.all option which is required for node 20.x
lookup = (hostname, opt, cb) => { _lookup(hostname, opt, (err, arg0, arg1) => { if (err) { return cb(err); }
const addresses = utils.isArray(arg0) ? arg0.map(addr => buildAddressEntry(addr)) : [buildAddressEntry(arg0, arg1)];
opt.all ? cb(err, addresses) : cb(err, addresses[0].address, addresses[0].family); }); } }
// 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, config.allowAbsoluteUrls); const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined); 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 {onUploadProgress, onDownloadProgress} = config; 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) || utils.isFile(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({ maxRate: utils.toFiniteNumber(maxUploadRate) })], utils.noop);
onUploadProgress && data.on('progress', flushOnFinish( data, progressEventDecorator( contentLength, progressEventReducer(asyncDecorator(onUploadProgress), false, 3) ) )); }
// 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, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: {} };
// cacheable-lookup integration hotfix
!utils.isUndefined(lookup) && (options.lookup = lookup);
if (config.socketPath) { options.socketPath = config.socketPath; } else { options.hostname = parsed.hostname.startsWith("[") ? parsed.hostname.slice(1, -1) : 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 || maxDownloadRate) { const transformStream = new AxiosTransformStream({ maxRate: utils.toFiniteNumber(maxDownloadRate) });
onDownloadProgress && transformStream.on('progress', flushOnFinish( transformStream, progressEventDecorator( responseLength, progressEventReducer(asyncDecorator(onDownloadProgress), true, 3) ) ));
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'] || '').toLowerCase()) { /*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( 'stream has been aborted', 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) { return 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 (Number.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;
|