UNPKG

axios

Version:

Promise based HTTP client for the browser and node.js

768 lines (692 loc) 23.9 kB
'use strict'; var utils = require('./../utils'); var settle = require('./../core/settle'); var buildFullPath = require('../core/buildFullPath'); var buildURL = require('./../helpers/buildURL'); var getProxyForUrl = require('proxy-from-env').getProxyForUrl; var http = require('http'); var https = require('https'); var httpFollow = require('follow-redirects/http'); var httpsFollow = require('follow-redirects/https'); var url = require('url'); var path = require('path'); var zlib = require('zlib'); var VERSION = require('./../env/data').version; var transitionalDefaults = require('../defaults/transitional'); var AxiosError = require('../core/AxiosError'); var CanceledError = require('../cancel/CanceledError'); var platform = require('../platform'); var fromDataURI = require('../helpers/fromDataURI'); var stream = require('stream'); var estimateDataURLDecodedBytes = require('../helpers/estimateDataURLDecodedBytes.js'); var shouldBypassProxy = require('../helpers/shouldBypassProxy'); var isHttps = /https:?/; var supportedProtocols = platform.protocols.map(function(protocol) { return protocol + ':'; }); function dispatchBeforeRedirect(options) { if (options.beforeRedirects.proxy) { options.beforeRedirects.proxy(options); } if (options.beforeRedirects.config) { options.beforeRedirects.config(options); } } function removeProxyAuthorization(headers) { Object.keys(headers).forEach(function removeHeader(header) { if (header.toLowerCase() === 'proxy-authorization') { delete headers[header]; } }); } function normalizeSocketPath(socketPath) { if (/^\\\\[.?]\\pipe\\/i.test(socketPath)) { return socketPath; } return path.resolve(socketPath); } function getAllowedSocketPaths(config) { var allowedSocketPaths = config.allowedSocketPaths; if ( allowedSocketPaths === null || typeof allowedSocketPaths === 'undefined' ) { return null; } if (utils.isString(allowedSocketPaths)) { return [allowedSocketPaths]; } if (utils.isArray(allowedSocketPaths)) { for (var i = 0; i < allowedSocketPaths.length; i++) { if (!utils.isString(allowedSocketPaths[i])) { return false; } } return allowedSocketPaths; } return false; } function checkSocketPath(config) { var socketPath = config.socketPath; if (!socketPath) { return null; } if (!utils.isString(socketPath)) { return new AxiosError( 'config.socketPath must be a string', AxiosError.ERR_BAD_OPTION_VALUE, config ); } var allowedSocketPaths = getAllowedSocketPaths(config); if (allowedSocketPaths === false) { return new AxiosError( 'config.allowedSocketPaths must be a string, an array of strings, or null', AxiosError.ERR_BAD_OPTION_VALUE, config ); } if (allowedSocketPaths) { var normalizedSocketPath = normalizeSocketPath(socketPath); var allowed = allowedSocketPaths.some( function isAllowed(allowedSocketPath) { return normalizeSocketPath(allowedSocketPath) === normalizedSocketPath; } ); if (!allowed) { return new AxiosError( 'config.socketPath is not allowed by config.allowedSocketPaths', AxiosError.ERR_BAD_OPTION_VALUE, config ); } } return null; } /** * * @param {http.ClientRequestArgs} options * @param {AxiosProxyConfig} configProxy * @param {string} location */ function setProxy(options, configProxy, location) { var proxy = configProxy; if (!proxy && proxy !== false) { var proxyUrl = getProxyForUrl(location); if (proxyUrl) { if (!shouldBypassProxy(location)) { proxy = url.parse(proxyUrl); // replace 'host' since the proxy object is not a URL object proxy.host = proxy.hostname; } } } if (!proxy) { removeProxyAuthorization(options.headers); } if (proxy) { // Basic proxy authorization var proxyAuth = utils.hasOwnProperty(proxy, 'auth') ? proxy.auth : undefined; if (proxyAuth) { // Support proxy auth object form if (utils.isObject(proxyAuth)) { var proxyUsername = utils.hasOwnProperty(proxyAuth, 'username') ? proxyAuth.username : ''; var proxyPassword = utils.hasOwnProperty(proxyAuth, 'password') ? proxyAuth.password : ''; proxyAuth = proxyUsername || proxyPassword ? proxyUsername + ':' + proxyPassword : undefined; } if (proxyAuth) { var base64 = Buffer.from(proxyAuth, 'utf8').toString('base64'); removeProxyAuthorization(options.headers); options.headers['Proxy-Authorization'] = 'Basic ' + base64; } } options.headers.host = options.hostname + (options.port ? ':' + options.port : ''); options.hostname = proxy.host; options.host = proxy.host; options.port = proxy.port; options.path = location; if (proxy.protocol) { options.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); }; } /*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest( resolvePromise, rejectPromise ) { var onCanceled; function done() { if (config.cancelToken) { config.cancelToken.unsubscribe(onCanceled); } if (config.signal) { config.signal.removeEventListener('abort', onCanceled); } } var resolve = function resolve(value) { done(); resolvePromise(value); }; var rejected = false; var reject = function reject(value) { done(); rejected = true; rejectPromise(value); }; var data = config.data; var responseType = config.responseType; var responseEncoding = config.responseEncoding; var method = config.method.toUpperCase(); // Parse url var fullPath = buildFullPath( config.baseURL, config.url, config.allowAbsoluteUrls ); var parsed = url.parse(fullPath); var 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. var dataUrl = String(config.url || fullPath || ''); var estimated = estimateDataURLDecodedBytes(dataUrl); if (estimated > config.maxContentLength) { return reject( new AxiosError( 'maxContentLength size of ' + config.maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config ) ); } } var convertedData; if (method !== 'GET') { return settle(resolve, reject, { status: 405, statusText: 'method not allowed', headers: {}, config: config }); } try { var envOption = utils.hasOwnProperty(config, 'env') ? config.env : undefined; convertedData = fromDataURI(config.url, responseType === 'blob', { Blob: envOption && envOption.Blob }); } catch (err) { throw AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config); } if (responseType === 'text') { convertedData = convertedData.toString(responseEncoding); if (!responseEncoding || responseEncoding === 'utf8') { data = utils.stripBOM(convertedData); } } else if (responseType === 'stream') { convertedData = stream.Readable.from(convertedData); } return settle(resolve, reject, { data: convertedData, status: 200, statusText: 'OK', headers: {}, config: config }); } if (supportedProtocols.indexOf(protocol) === -1) { return reject( new AxiosError( 'Unsupported protocol ' + protocol, AxiosError.ERR_BAD_REQUEST, config ) ); } var headers = config.headers; var headerNames = {}; Object.keys(headers).forEach(function storeLowerName(name) { headerNames[name.toLowerCase()] = name; }); // Set User-Agent (required by some servers) // See https://github.com/axios/axios/issues/69 if ('user-agent' in headerNames) { // User-Agent is specified; handle case where no UA header is desired if (!headers[headerNames['user-agent']]) { delete headers[headerNames['user-agent']]; } // Otherwise, use specified value } else { // Only set header if it hasn't been set in config headers['User-Agent'] = 'axios/' + VERSION; } // support for https://www.npmjs.com/package/form-data api if ( utils.isFormData(data) && utils.isFunction(data.getHeaders) && data.getHeaders !== Object.prototype.getHeaders ) { var formHeaders = data.getHeaders(); if (config.formDataHeaderPolicy === 'content-only') { Object.keys(formHeaders).forEach(function copyContentHeader(name) { var lowerName = name.toLowerCase(); if (lowerName === 'content-type' || lowerName === 'content-length') { headers[name] = formHeaders[name]; } }); } else { Object.assign(headers, formHeaders); } } 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 ) ); } if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) { return reject( new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config ) ); } // Add Content-Length header if data exists if (!headerNames['content-length']) { headers['Content-Length'] = data.length; } } // HTTP basic authentication var auth = undefined; if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; auth = username + ':' + password; } if (!auth && parsed.auth) { var urlAuth = parsed.auth.split(':'); var urlUsername = urlAuth[0] || ''; var urlPassword = urlAuth[1] || ''; auth = urlUsername + ':' + urlPassword; } if (auth && headerNames.authorization) { delete headers[headerNames.authorization]; } try { buildURL(parsed.path, config.params, config.paramsSerializer).replace( /^\?/, '' ); } catch (err) { var customErr = new Error(err.message); customErr.config = config; customErr.url = config.url; customErr.exists = true; reject(customErr); } var options = { path: buildURL( parsed.path, config.params, config.paramsSerializer ).replace(/^\?/, ''), method: method, headers: headers, agents: { http: config.httpAgent, https: config.httpsAgent }, auth: auth, protocol: protocol, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: {} }; var socketPathError = checkSocketPath(config); if (socketPathError) { return reject(socketPathError); } if (config.socketPath) { options.socketPath = config.socketPath; } else { options.hostname = parsed.hostname; options.port = parsed.port; setProxy( options, config.proxy, protocol + '//' + parsed.host + options.path ); } var transport; var isHttpsRequest = isHttps.test(options.protocol); options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; if (utils.hasOwnProperty(config, 'transport') && 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 var req = transport.request(options, function handleResponse(res) { if (req.aborted) return; // uncompress the response body transparently if required var responseStream = res; // return the last request in case of redirects var lastRequest = res.req || req; // if decompress disabled we should not decompress if (config.decompress !== false) { // if no content, but headers still say that it is encoded, // remove the header not confuse downstream operations if (data && data.length === 0 && res.headers['content-encoding']) { delete res.headers['content-encoding']; } switch (res.headers['content-encoding']) { /*eslint default-case:0*/ case 'gzip': case 'compress': case 'deflate': // add the unzipper to the body stream processing pipeline responseStream = responseStream.pipe(zlib.createUnzip()); // remove the content-encoding in order to not confuse downstream operations delete res.headers['content-encoding']; break; } } var response = { status: res.statusCode, statusText: res.statusMessage, headers: res.headers, config: config, request: lastRequest }; if (responseType === 'stream') { // Enforce maxContentLength on streamed responses too. // Previously the stream path bypassed the size guard because the check only // ran on the buffering branch. if (config.maxContentLength > -1) { var maxContentLength = config.maxContentLength; var streamedBytes = 0; var limiter = new stream.Transform({ transform: function transformChunk(chunk, encoding, callback) { streamedBytes += chunk.length; if (streamedBytes > maxContentLength) { callback( new AxiosError( 'maxContentLength size of ' + maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, lastRequest ) ); return; } callback(null, chunk); } }); limiter.on('error', function handleLimiterError() { rejected = true; responseStream.destroy(); }); responseStream.on('error', function forwardError(err) { limiter.destroy(err); }); response.data = limiter; settle(resolve, reject, response); // Defer piping via setImmediate so the caller's `.then` (a microtask) // has run and attached any `error`/`data` listeners before chunks flow // through the transform. `process.nextTick` would drain before those // microtasks and lose the error event. setImmediate(function startPipe() { responseStream.pipe(limiter); }); } else { response.data = responseStream; settle(resolve, reject, response); } } else { var responseBuffer = []; var 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; } responseStream.destroy(); reject( new AxiosError( 'response stream aborted', AxiosError.ECONNABORTED, config, lastRequest ) ); }); responseStream.on('error', function handleStreamError(err) { if (req.aborted) return; reject(AxiosError.from(err, null, config, lastRequest)); }); responseStream.on('end', function handleStreamEnd() { try { var 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); }); } }); // 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) { // Force an int timeout so the `req` interface gets a clean number. // The try/catch is required: merged config values have a null prototype, // and parseInt on a null-prototype object throws "Cannot convert object // to primitive value" because there is no inherited toString. Treating // that as NaN routes to the same ERR_BAD_OPTION_VALUE rejection as any // other unparsable value. var timeout; try { timeout = parseInt(config.timeout, 10); } catch (err) { timeout = NaN; } 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() { req.abort(); var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; var transitional = config.transitional || transitionalDefaults; if (config.timeoutErrorMessage) { timeoutErrorMessage = config.timeoutErrorMessage; } reject( new AxiosError( timeoutErrorMessage, transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, req ) ); }); } if (config.cancelToken || config.signal) { // Handle cancellation // eslint-disable-next-line func-names onCanceled = function(cancel) { if (req.aborted) return; req.abort(); reject( !cancel || cancel.type ? new CanceledError(null, config, req) : cancel ); }; config.cancelToken && config.cancelToken.subscribe(onCanceled); if (config.signal) { config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled); } } // Send the request if (utils.isStream(data)) { data.on('error', function handleStreamError(err) { reject(AxiosError.from(err, config, null, req)); }); // follow-redirects enforces options.maxBodyLength for stream uploads, but the // native http/https transport (used when maxRedirects === 0) does not. // Count bytes ourselves so the limit is always honored. var nativeTransport = transport === http || transport === https; if (nativeTransport && config.maxBodyLength > -1) { var maxBodyLength = config.maxBodyLength; var uploadedBytes = 0; var bodyLimiter = new stream.Transform({ transform: function transformChunk(chunk, encoding, callback) { uploadedBytes += chunk.length; if (uploadedBytes > maxBodyLength) { callback( new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config, req ) ); return; } callback(null, chunk); } }); bodyLimiter.on('error', function handleLimiterError(err) { if (rejected) return; rejected = true; try { data.unpipe(bodyLimiter); } catch (e) { /* noop */ } try { bodyLimiter.unpipe(req); } catch (e) { /* noop */ } req.destroy(); reject(err); }); data.pipe(bodyLimiter).pipe(req); } else { data.pipe(req); } } else { req.end(data); } }); };