mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
cd0d44825f
* fix(http): preserve response in error when stream is aborted after headers When a server sends response headers but aborts the stream before completing the body, axios now attaches the response object to the error. This allows consumers to access response metadata (status, headers) for debugging, retry logic, or user messaging. Fixes #6935 * fix(http): normalize response stream errors --------- Co-authored-by: Jay <jasonsaayman@gmail.com>
1110 lines
35 KiB
JavaScript
Executable File
1110 lines
35 KiB
JavaScript
Executable File
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 http2 from 'http2';
|
|
import util from 'util';
|
|
import { resolve as resolvePath } from 'path';
|
|
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 shouldBypassProxy from '../helpers/shouldBypassProxy.js';
|
|
import {
|
|
progressEventReducer,
|
|
progressEventDecorator,
|
|
asyncDecorator,
|
|
} from '../helpers/progressEventReducer.js';
|
|
import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.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:?/;
|
|
|
|
// Symbols used to bind a single 'error' listener to a pooled socket and track
|
|
// the request currently owning that socket across keep-alive reuse (issue #10780).
|
|
const kAxiosSocketListener = Symbol('axios.http.socketListener');
|
|
const kAxiosCurrentReq = Symbol('axios.http.currentReq');
|
|
|
|
const supportedProtocols = platform.protocols.map((protocol) => {
|
|
return protocol + ':';
|
|
});
|
|
|
|
const flushOnFinish = (stream, [throttled, flush]) => {
|
|
stream.on('end', flush).on('error', flush);
|
|
|
|
return throttled;
|
|
};
|
|
|
|
class Http2Sessions {
|
|
constructor() {
|
|
this.sessions = Object.create(null);
|
|
}
|
|
|
|
getSession(authority, options) {
|
|
options = Object.assign(
|
|
{
|
|
sessionTimeout: 1000,
|
|
},
|
|
options
|
|
);
|
|
|
|
let authoritySessions = this.sessions[authority];
|
|
|
|
if (authoritySessions) {
|
|
let len = authoritySessions.length;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
const [sessionHandle, sessionOptions] = authoritySessions[i];
|
|
if (
|
|
!sessionHandle.destroyed &&
|
|
!sessionHandle.closed &&
|
|
util.isDeepStrictEqual(sessionOptions, options)
|
|
) {
|
|
return sessionHandle;
|
|
}
|
|
}
|
|
}
|
|
|
|
const session = http2.connect(authority, options);
|
|
|
|
let removed;
|
|
|
|
const removeSession = () => {
|
|
if (removed) {
|
|
return;
|
|
}
|
|
|
|
removed = true;
|
|
|
|
let entries = authoritySessions,
|
|
len = entries.length,
|
|
i = len;
|
|
|
|
while (i--) {
|
|
if (entries[i][0] === session) {
|
|
if (len === 1) {
|
|
delete this.sessions[authority];
|
|
} else {
|
|
entries.splice(i, 1);
|
|
}
|
|
if (!session.closed) {
|
|
session.close();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
const originalRequestFn = session.request;
|
|
|
|
const { sessionTimeout } = options;
|
|
|
|
if (sessionTimeout != null) {
|
|
let timer;
|
|
let streamsCount = 0;
|
|
|
|
session.request = function () {
|
|
const stream = originalRequestFn.apply(this, arguments);
|
|
|
|
streamsCount++;
|
|
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
|
|
stream.once('close', () => {
|
|
if (!--streamsCount) {
|
|
timer = setTimeout(() => {
|
|
timer = null;
|
|
removeSession();
|
|
}, sessionTimeout);
|
|
}
|
|
});
|
|
|
|
return stream;
|
|
};
|
|
}
|
|
|
|
session.once('close', removeSession);
|
|
|
|
let entry = [session, options];
|
|
|
|
authoritySessions
|
|
? authoritySessions.push(entry)
|
|
: (authoritySessions = this.sessions[authority] = [entry]);
|
|
|
|
return session;
|
|
}
|
|
}
|
|
|
|
const http2Sessions = new Http2Sessions();
|
|
|
|
/**
|
|
* 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, requestDetails) {
|
|
if (options.beforeRedirects.proxy) {
|
|
options.beforeRedirects.proxy(options);
|
|
}
|
|
if (options.beforeRedirects.config) {
|
|
options.beforeRedirects.config(options, responseDetails, requestDetails);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, isRedirect) {
|
|
let proxy = configProxy;
|
|
if (!proxy && proxy !== false) {
|
|
const proxyUrl = getProxyForUrl(location);
|
|
if (proxyUrl) {
|
|
if (!shouldBypassProxy(location)) {
|
|
proxy = new URL(proxyUrl);
|
|
}
|
|
}
|
|
}
|
|
// On redirect re-invocation, strip any stale Proxy-Authorization header carried
|
|
// over from the prior request (e.g. new target no longer uses a proxy, or uses
|
|
// a different proxy). Skip on the initial request so user-supplied headers are
|
|
// preserved. Header names are case-insensitive, so remove every case variant.
|
|
if (isRedirect && options.headers) {
|
|
for (const name of Object.keys(options.headers)) {
|
|
if (name.toLowerCase() === 'proxy-authorization') {
|
|
delete options.headers[name];
|
|
}
|
|
}
|
|
}
|
|
if (proxy) {
|
|
// Basic proxy authorization
|
|
if (proxy.username) {
|
|
proxy.auth = (proxy.username || '') + ':' + (proxy.password || '');
|
|
}
|
|
|
|
if (proxy.auth) {
|
|
// Support proxy auth object form
|
|
const validProxyAuth = Boolean(proxy.auth.username || proxy.auth.password);
|
|
|
|
if (validProxyAuth) {
|
|
proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');
|
|
} else if (typeof proxy.auth === 'object') {
|
|
throw new AxiosError('Invalid proxy authorization', AxiosError.ERR_BAD_OPTION, { proxy });
|
|
}
|
|
|
|
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, true);
|
|
};
|
|
}
|
|
|
|
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 });
|
|
|
|
const http2Transport = {
|
|
request(options, cb) {
|
|
const authority =
|
|
options.protocol +
|
|
'//' +
|
|
options.hostname +
|
|
':' +
|
|
(options.port || (options.protocol === 'https:' ? 443 : 80));
|
|
|
|
const { http2Options, headers } = options;
|
|
|
|
const session = http2Sessions.getSession(authority, http2Options);
|
|
|
|
const { HTTP2_HEADER_SCHEME, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_STATUS } =
|
|
http2.constants;
|
|
|
|
const http2Headers = {
|
|
[HTTP2_HEADER_SCHEME]: options.protocol.replace(':', ''),
|
|
[HTTP2_HEADER_METHOD]: options.method,
|
|
[HTTP2_HEADER_PATH]: options.path,
|
|
};
|
|
|
|
utils.forEach(headers, (header, name) => {
|
|
name.charAt(0) !== ':' && (http2Headers[name] = header);
|
|
});
|
|
|
|
const req = session.request(http2Headers);
|
|
|
|
req.once('response', (responseHeaders) => {
|
|
const response = req; //duplex
|
|
|
|
responseHeaders = Object.assign({}, responseHeaders);
|
|
|
|
const status = responseHeaders[HTTP2_HEADER_STATUS];
|
|
|
|
delete responseHeaders[HTTP2_HEADER_STATUS];
|
|
|
|
response.headers = responseHeaders;
|
|
|
|
response.statusCode = +status;
|
|
|
|
cb(response);
|
|
});
|
|
|
|
return req;
|
|
},
|
|
};
|
|
|
|
/*eslint consistent-return:0*/
|
|
export default isHttpAdapterSupported &&
|
|
function httpAdapter(config) {
|
|
return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {
|
|
const own = (key) => (utils.hasOwnProp(config, key) ? config[key] : undefined);
|
|
let data = own('data');
|
|
let lookup = own('lookup');
|
|
let family = own('family');
|
|
let httpVersion = own('httpVersion');
|
|
if (httpVersion === undefined) httpVersion = 1;
|
|
let http2Options = own('http2Options');
|
|
const responseType = own('responseType');
|
|
const responseEncoding = own('responseEncoding');
|
|
const method = config.method.toUpperCase();
|
|
let isDone;
|
|
let rejected = false;
|
|
let req;
|
|
|
|
httpVersion = +httpVersion;
|
|
|
|
if (Number.isNaN(httpVersion)) {
|
|
throw TypeError(`Invalid protocol version: '${config.httpVersion}' is not a number`);
|
|
}
|
|
|
|
if (httpVersion !== 1 && httpVersion !== 2) {
|
|
throw TypeError(`Unsupported protocol version '${httpVersion}'`);
|
|
}
|
|
|
|
const isHttp2 = httpVersion === 2;
|
|
|
|
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);
|
|
});
|
|
};
|
|
}
|
|
|
|
const abortEmitter = new EventEmitter();
|
|
|
|
function abort(reason) {
|
|
try {
|
|
abortEmitter.emit(
|
|
'abort',
|
|
!reason || reason.type ? new CanceledError(null, config, req) : reason
|
|
);
|
|
} catch (err) {
|
|
console.warn('emit error', err);
|
|
}
|
|
}
|
|
|
|
abortEmitter.once('abort', reject);
|
|
|
|
const onFinished = () => {
|
|
if (config.cancelToken) {
|
|
config.cancelToken.unsubscribe(abort);
|
|
}
|
|
|
|
if (config.signal) {
|
|
config.signal.removeEventListener('abort', abort);
|
|
}
|
|
|
|
abortEmitter.removeAllListeners();
|
|
};
|
|
|
|
if (config.cancelToken || config.signal) {
|
|
config.cancelToken && config.cancelToken.subscribe(abort);
|
|
if (config.signal) {
|
|
config.signal.aborted ? abort() : config.signal.addEventListener('abort', abort);
|
|
}
|
|
}
|
|
|
|
onDone((response, isRejected) => {
|
|
isDone = true;
|
|
|
|
if (isRejected) {
|
|
rejected = true;
|
|
onFinished();
|
|
return;
|
|
}
|
|
|
|
const { data } = response;
|
|
|
|
if (data instanceof stream.Readable || data instanceof stream.Duplex) {
|
|
const offListeners = stream.finished(data, () => {
|
|
offListeners();
|
|
onFinished();
|
|
});
|
|
} else {
|
|
onFinished();
|
|
}
|
|
});
|
|
|
|
// 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:') {
|
|
// Apply the same semantics as HTTP: only enforce if a finite, non-negative cap is set.
|
|
if (config.maxContentLength > -1) {
|
|
// Use the exact string passed to fromDataURI (config.url); fall back to fullPath if needed.
|
|
const dataUrl = String(config.url || fullPath || '');
|
|
const estimated = estimateDataURLDecodedBytes(dataUrl);
|
|
|
|
if (estimated > config.maxContentLength) {
|
|
return reject(
|
|
new AxiosError(
|
|
'maxContentLength size of ' + config.maxContentLength + ' exceeded',
|
|
AxiosError.ERR_BAD_RESPONSE,
|
|
config
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
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) &&
|
|
data.getHeaders !== Object.prototype.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;
|
|
const configAuth = own('auth');
|
|
if (configAuth) {
|
|
const username = configAuth.username || '';
|
|
const password = configAuth.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
|
|
);
|
|
|
|
// Null-prototype to block prototype pollution gadgets on properties read
|
|
// directly by Node's http.request (e.g. insecureHTTPParser, lookup).
|
|
// See GHSA-q8qp-cvcw-x6jj.
|
|
const options = Object.assign(Object.create(null), {
|
|
path,
|
|
method: method,
|
|
headers: headers.toJSON(),
|
|
agents: { http: config.httpAgent, https: config.httpsAgent },
|
|
auth,
|
|
protocol,
|
|
family,
|
|
beforeRedirect: dispatchBeforeRedirect,
|
|
beforeRedirects: Object.create(null),
|
|
http2Options,
|
|
});
|
|
|
|
// cacheable-lookup integration hotfix
|
|
!utils.isUndefined(lookup) && (options.lookup = lookup);
|
|
|
|
if (config.socketPath) {
|
|
if (typeof config.socketPath !== 'string') {
|
|
return reject(new AxiosError(
|
|
'socketPath must be a string',
|
|
AxiosError.ERR_BAD_OPTION_VALUE,
|
|
config
|
|
));
|
|
}
|
|
|
|
if (config.allowedSocketPaths != null) {
|
|
const allowed = Array.isArray(config.allowedSocketPaths)
|
|
? config.allowedSocketPaths
|
|
: [config.allowedSocketPaths];
|
|
|
|
const resolvedSocket = resolvePath(config.socketPath);
|
|
const isAllowed = allowed.some(
|
|
(entry) => typeof entry === 'string' && resolvePath(entry) === resolvedSocket
|
|
);
|
|
|
|
if (!isAllowed) {
|
|
return reject(new AxiosError(
|
|
`socketPath "${config.socketPath}" is not permitted by allowedSocketPaths`,
|
|
AxiosError.ERR_BAD_OPTION_VALUE,
|
|
config
|
|
));
|
|
}
|
|
}
|
|
|
|
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 (isHttp2) {
|
|
transport = http2Transport;
|
|
} else {
|
|
const configTransport = own('transport');
|
|
if (configTransport) {
|
|
transport = configTransport;
|
|
} else if (config.maxRedirects === 0) {
|
|
transport = isHttpsRequest ? https : http;
|
|
} else {
|
|
if (config.maxRedirects) {
|
|
options.maxRedirects = config.maxRedirects;
|
|
}
|
|
const configBeforeRedirect = own('beforeRedirect');
|
|
if (configBeforeRedirect) {
|
|
options.beforeRedirects.config = configBeforeRedirect;
|
|
}
|
|
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;
|
|
}
|
|
|
|
// Always set an explicit own value so a polluted
|
|
// Object.prototype.insecureHTTPParser cannot enable the lenient parser
|
|
// through Node's internal options copy (GHSA-q8qp-cvcw-x6jj).
|
|
options.insecureHTTPParser = Boolean(own('insecureHTTPParser'));
|
|
|
|
// Create the request
|
|
req = transport.request(options, function handleResponse(res) {
|
|
if (req.destroyed) return;
|
|
|
|
const streams = [res];
|
|
|
|
const responseLength = utils.toFiniteNumber(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 response = {
|
|
status: res.statusCode,
|
|
statusText: res.statusMessage,
|
|
headers: new AxiosHeaders(res.headers),
|
|
config,
|
|
request: lastRequest,
|
|
};
|
|
|
|
if (responseType === 'stream') {
|
|
// Enforce maxContentLength on streamed responses; previously this
|
|
// was applied only to buffered responses. See GHSA-vf2m-468p-8v99.
|
|
if (config.maxContentLength > -1) {
|
|
const limit = config.maxContentLength;
|
|
const source = responseStream;
|
|
async function* enforceMaxContentLength() {
|
|
let totalResponseBytes = 0;
|
|
for await (const chunk of source) {
|
|
totalResponseBytes += chunk.length;
|
|
if (totalResponseBytes > limit) {
|
|
throw new AxiosError(
|
|
'maxContentLength size of ' + limit + ' exceeded',
|
|
AxiosError.ERR_BAD_RESPONSE,
|
|
config,
|
|
lastRequest
|
|
);
|
|
}
|
|
yield chunk;
|
|
}
|
|
}
|
|
responseStream = stream.Readable.from(enforceMaxContentLength(), {
|
|
objectMode: false,
|
|
});
|
|
}
|
|
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();
|
|
abort(
|
|
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,
|
|
response
|
|
);
|
|
responseStream.destroy(err);
|
|
reject(err);
|
|
});
|
|
|
|
responseStream.on('error', function handleStreamError(err) {
|
|
if (req.destroyed) return;
|
|
reject(AxiosError.from(err, null, config, lastRequest, response));
|
|
});
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
abortEmitter.once('abort', (err) => {
|
|
if (!responseStream.destroyed) {
|
|
responseStream.emit('error', err);
|
|
responseStream.destroy();
|
|
}
|
|
});
|
|
});
|
|
|
|
abortEmitter.once('abort', (err) => {
|
|
if (req.close) {
|
|
req.close();
|
|
} else {
|
|
req.destroy(err);
|
|
}
|
|
});
|
|
|
|
// Handle errors
|
|
req.on('error', function handleRequestError(err) {
|
|
reject(AxiosError.from(err, null, config, req));
|
|
});
|
|
|
|
// set tcp keep alive to prevent drop connection by peer
|
|
// Track every socket bound to this outer RedirectableRequest so a single
|
|
// 'close' listener can release ownership on all of them. follow-redirects
|
|
// re-emits the 'socket' event for each hop's native request onto the same
|
|
// outer request, so attaching per-request listeners inside this handler
|
|
// would accumulate across hops and trigger MaxListenersExceededWarning at
|
|
// >= 11 redirects. Clearing only the last-bound socket would leave stale
|
|
// kAxiosCurrentReq refs on earlier hop sockets returned to the keep-alive
|
|
// pool, causing an idle-pool 'error' to be attributed to a closed req.
|
|
const boundSockets = new Set();
|
|
|
|
req.on('socket', function handleRequestSocket(socket) {
|
|
// default interval of sending ack packet is 1 minute
|
|
socket.setKeepAlive(true, 1000 * 60);
|
|
|
|
// Install a single 'error' listener per socket (not per request) to avoid
|
|
// accumulating listeners on pooled keep-alive sockets that get reassigned
|
|
// to new requests before the previous request's 'close' fires (issue #10780).
|
|
// The listener is bound to the socket's currently-active request via a
|
|
// symbol, which is swapped as the socket is reassigned.
|
|
if (!socket[kAxiosSocketListener]) {
|
|
socket.on('error', function handleSocketError(err) {
|
|
const current = socket[kAxiosCurrentReq];
|
|
if (current && !current.destroyed) {
|
|
current.destroy(err);
|
|
}
|
|
});
|
|
socket[kAxiosSocketListener] = true;
|
|
}
|
|
|
|
socket[kAxiosCurrentReq] = req;
|
|
boundSockets.add(socket);
|
|
});
|
|
|
|
req.once('close', function clearCurrentReq() {
|
|
for (const socket of boundSockets) {
|
|
if (socket[kAxiosCurrentReq] === req) {
|
|
socket[kAxiosCurrentReq] = null;
|
|
}
|
|
}
|
|
boundSockets.clear();
|
|
});
|
|
|
|
// 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)) {
|
|
abort(
|
|
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;
|
|
}
|
|
abort(
|
|
new AxiosError(
|
|
timeoutErrorMessage,
|
|
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
|
|
config,
|
|
req
|
|
)
|
|
);
|
|
});
|
|
} else {
|
|
// explicitly reset the socket timeout value for a possible `keep-alive` request
|
|
req.setTimeout(0);
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
});
|
|
|
|
// Enforce maxBodyLength for streamed uploads on the native http/https
|
|
// transport (maxRedirects === 0); follow-redirects enforces it on the
|
|
// other path. See GHSA-5c9x-8gcm-mpgx.
|
|
let uploadStream = data;
|
|
if (config.maxBodyLength > -1 && config.maxRedirects === 0) {
|
|
const limit = config.maxBodyLength;
|
|
let bytesSent = 0;
|
|
uploadStream = stream.pipeline(
|
|
[
|
|
data,
|
|
new stream.Transform({
|
|
transform(chunk, _enc, cb) {
|
|
bytesSent += chunk.length;
|
|
if (bytesSent > limit) {
|
|
return cb(
|
|
new AxiosError(
|
|
'Request body larger than maxBodyLength limit',
|
|
AxiosError.ERR_BAD_REQUEST,
|
|
config,
|
|
req
|
|
)
|
|
);
|
|
}
|
|
cb(null, chunk);
|
|
},
|
|
}),
|
|
],
|
|
utils.noop
|
|
);
|
|
uploadStream.on('error', (err) => {
|
|
if (!req.destroyed) req.destroy(err);
|
|
});
|
|
}
|
|
|
|
uploadStream.pipe(req);
|
|
} else {
|
|
data && req.write(data);
|
|
req.end();
|
|
}
|
|
});
|
|
};
|
|
|
|
export const __setProxy = setProxy;
|