mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
Axios ES2017 (#4787)
* Added AxiosHeaders class; * Fixed README.md href; * Fixed a potential bug with headers normalization; * Fixed a potential bug with headers normalization; Refactored accessor building routine; Refactored default transforms; Removed `normalizeHeaderName` helper; * Added `Content-Length` accessor; Added missed `has` accessor to TS types; * Added `AxiosTransformStream` class; Added progress capturing ability for node.js environment; Added `maxRate` option to limit the data rate in node.js environment; Refactored event handled by `onUploadProgress` && `onDownloadProgress` listeners in browser environment; Added progress & data rate tests for the http adapter; Added response stream aborting test; Added a manual progress capture test for the browser; Updated TS types; Added TS tests; Refactored request abort logic for the http adapter; Added ability to abort the response stream; * Remove `stream/promises` & `timers/promises` modules usage in tests; * Use `abortcontroller-polyfill`; * Fixed AxiosTransformStream dead-lock in legacy node versions; Fixed CancelError emitting in streams; * Reworked AxiosTransformStream internal logic to optimize memory consumption; Added throwing an error if the request stream was silently destroying (without error) Refers to #3966; * Treat the destruction of the request stream as a cancellation of the request; Fixed tests; * Emit `progress` event in the next tick; * Initial refactoring; * Refactored Mocha tests to use ESM; * Refactored Karma tests to use rollup preprocessor & ESM; Replaced grunt with gulp; Improved dev scripts; Added Babel for rollup build; * Added default commonjs package export for Node build; Added automatic contributors list generator for package.json; Co-authored-by: Jay <jasonsaayman@gmail.com>
This commit is contained in:
+229
-121
@@ -1,27 +1,33 @@
|
||||
'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 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');
|
||||
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 followRedirects from 'follow-redirects';
|
||||
import url from 'url';
|
||||
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';
|
||||
|
||||
var isHttps = /https:?/;
|
||||
const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);
|
||||
|
||||
var supportedProtocols = platform.protocols.map(function(protocol) {
|
||||
const {http: httpFollow, https: httpsFollow} = followRedirects;
|
||||
|
||||
const isHttps = /https:?/;
|
||||
|
||||
const supportedProtocols = platform.protocols.map(protocol => {
|
||||
return protocol + ':';
|
||||
});
|
||||
|
||||
@@ -52,9 +58,9 @@ function dispatchBeforeRedirect(options) {
|
||||
* @returns {http.ClientRequestArgs}
|
||||
*/
|
||||
function setProxy(options, configProxy, location) {
|
||||
var proxy = configProxy;
|
||||
let proxy = configProxy;
|
||||
if (!proxy && proxy !== false) {
|
||||
var proxyUrl = getProxyForUrl(location);
|
||||
const proxyUrl = getProxyForUrl(location);
|
||||
if (proxyUrl) {
|
||||
proxy = url.parse(proxyUrl);
|
||||
// replace 'host' since the proxy object is not a URL object
|
||||
@@ -68,7 +74,7 @@ function setProxy(options, configProxy, location) {
|
||||
if (proxy.auth.username || proxy.auth.password) {
|
||||
proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');
|
||||
}
|
||||
var base64 = Buffer
|
||||
const base64 = Buffer
|
||||
.from(proxy.auth, 'utf8')
|
||||
.toString('base64');
|
||||
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
|
||||
@@ -92,10 +98,25 @@ function setProxy(options, configProxy, location) {
|
||||
}
|
||||
|
||||
/*eslint consistent-return:0*/
|
||||
module.exports = function httpAdapter(config) {
|
||||
export default function httpAdapter(config) {
|
||||
return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
|
||||
var onCanceled;
|
||||
function done() {
|
||||
let onCanceled;
|
||||
let data = config.data;
|
||||
const responseType = config.responseType;
|
||||
const responseEncoding = config.responseEncoding;
|
||||
const method = config.method.toUpperCase();
|
||||
let isFinished;
|
||||
let isDone;
|
||||
let rejected = false;
|
||||
let req;
|
||||
|
||||
// temporary internal emitter until the AxiosRequest class will be implemented
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
function onFinished() {
|
||||
if (isFinished) return;
|
||||
isFinished = true;
|
||||
|
||||
if (config.cancelToken) {
|
||||
config.cancelToken.unsubscribe(onCanceled);
|
||||
}
|
||||
@@ -103,36 +124,58 @@ module.exports = function httpAdapter(config) {
|
||||
if (config.signal) {
|
||||
config.signal.removeEventListener('abort', onCanceled);
|
||||
}
|
||||
|
||||
emitter.removeAllListeners();
|
||||
}
|
||||
var resolve = function resolve(value) {
|
||||
done();
|
||||
resolvePromise(value);
|
||||
|
||||
function done(value, isRejected) {
|
||||
if (isDone) return;
|
||||
|
||||
isDone = true;
|
||||
|
||||
if (isRejected) {
|
||||
rejected = true;
|
||||
onFinished();
|
||||
}
|
||||
|
||||
isRejected ? rejectPromise(value) : resolvePromise(value);
|
||||
}
|
||||
|
||||
const resolve = function resolve(value) {
|
||||
done(value);
|
||||
};
|
||||
var rejected = false;
|
||||
var reject = function reject(value) {
|
||||
done();
|
||||
rejected = true;
|
||||
rejectPromise(value);
|
||||
|
||||
const reject = function reject(value) {
|
||||
done(value, true);
|
||||
};
|
||||
var data = config.data;
|
||||
var responseType = config.responseType;
|
||||
var responseEncoding = config.responseEncoding;
|
||||
var method = config.method.toUpperCase();
|
||||
|
||||
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
|
||||
var fullPath = buildFullPath(config.baseURL, config.url);
|
||||
var parsed = url.parse(fullPath);
|
||||
var protocol = parsed.protocol || supportedProtocols[0];
|
||||
const fullPath = buildFullPath(config.baseURL, config.url);
|
||||
const parsed = url.parse(fullPath);
|
||||
const protocol = parsed.protocol || supportedProtocols[0];
|
||||
|
||||
if (protocol === 'data:') {
|
||||
var convertedData;
|
||||
let convertedData;
|
||||
|
||||
if (method !== 'GET') {
|
||||
return settle(resolve, reject, {
|
||||
status: 405,
|
||||
statusText: 'method not allowed',
|
||||
headers: {},
|
||||
config: config
|
||||
config
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,7 +202,7 @@ module.exports = function httpAdapter(config) {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: config
|
||||
config
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,29 +214,23 @@ module.exports = function httpAdapter(config) {
|
||||
));
|
||||
}
|
||||
|
||||
var headers = config.headers;
|
||||
var headerNames = {};
|
||||
|
||||
Object.keys(headers).forEach(function storeLowerName(name) {
|
||||
headerNames[name.toLowerCase()] = name;
|
||||
});
|
||||
const headers = AxiosHeaders.from(config.headers).normalize();
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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 https://www.npmjs.com/package/form-data api
|
||||
if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) {
|
||||
Object.assign(headers, data.getHeaders());
|
||||
headers.set(data.getHeaders());
|
||||
} else if (data && !utils.isStream(data)) {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
// Nothing to do...
|
||||
@@ -209,6 +246,9 @@ module.exports = function httpAdapter(config) {
|
||||
));
|
||||
}
|
||||
|
||||
// Add Content-Length header if data exists
|
||||
headers.set('Content-Length', data.length, false);
|
||||
|
||||
if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) {
|
||||
return reject(new AxiosError(
|
||||
'Request body larger than maxBodyLength limit',
|
||||
@@ -216,49 +256,70 @@ module.exports = function httpAdapter(config) {
|
||||
config
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Add Content-Length header if data exists
|
||||
if (!headerNames['content-length']) {
|
||||
headers['Content-Length'] = data.length;
|
||||
const contentLength = +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: utils.toFiniteNumber(contentLength),
|
||||
maxRate: utils.toFiniteNumber(maxUploadRate)
|
||||
})], utils.noop);
|
||||
|
||||
onUploadProgress && data.on('progress', progress => {
|
||||
onUploadProgress(Object.assign(progress, {
|
||||
upload: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// HTTP basic authentication
|
||||
var auth = undefined;
|
||||
let auth = undefined;
|
||||
if (config.auth) {
|
||||
var username = config.auth.username || '';
|
||||
var password = config.auth.password || '';
|
||||
const username = config.auth.username || '';
|
||||
const password = config.auth.password || '';
|
||||
auth = username + ':' + password;
|
||||
}
|
||||
|
||||
if (!auth && parsed.auth) {
|
||||
var urlAuth = parsed.auth.split(':');
|
||||
var urlUsername = urlAuth[0] || '';
|
||||
var urlPassword = urlAuth[1] || '';
|
||||
const urlAuth = parsed.auth.split(':');
|
||||
const urlUsername = urlAuth[0] || '';
|
||||
const urlPassword = urlAuth[1] || '';
|
||||
auth = urlUsername + ':' + urlPassword;
|
||||
}
|
||||
|
||||
if (auth && headerNames.authorization) {
|
||||
delete headers[headerNames.authorization];
|
||||
}
|
||||
auth && headers.delete('authorization');
|
||||
|
||||
try {
|
||||
buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, '');
|
||||
} catch (err) {
|
||||
var customErr = new Error(err.message);
|
||||
const customErr = new Error(err.message);
|
||||
customErr.config = config;
|
||||
customErr.url = config.url;
|
||||
customErr.exists = true;
|
||||
reject(customErr);
|
||||
return reject(customErr);
|
||||
}
|
||||
|
||||
var options = {
|
||||
headers.set('Accept-Encoding', 'gzip, deflate, gzip, br', false);
|
||||
|
||||
const options = {
|
||||
path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
|
||||
method: method,
|
||||
headers: headers,
|
||||
method,
|
||||
headers: headers.toJSON(),
|
||||
agents: { http: config.httpAgent, https: config.httpsAgent },
|
||||
auth: auth,
|
||||
protocol: protocol,
|
||||
auth,
|
||||
protocol,
|
||||
beforeRedirect: dispatchBeforeRedirect,
|
||||
beforeRedirects: {}
|
||||
};
|
||||
@@ -271,8 +332,8 @@ module.exports = function httpAdapter(config) {
|
||||
setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
|
||||
}
|
||||
|
||||
var transport;
|
||||
var isHttpsRequest = isHttps.test(options.protocol);
|
||||
let transport;
|
||||
const isHttpsRequest = isHttps.test(options.protocol);
|
||||
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
|
||||
if (config.transport) {
|
||||
transport = config.transport;
|
||||
@@ -300,14 +361,16 @@ module.exports = function httpAdapter(config) {
|
||||
}
|
||||
|
||||
// Create the request
|
||||
var req = transport.request(options, function handleResponse(res) {
|
||||
if (req.aborted) return;
|
||||
req = transport.request(options, function handleResponse(res) {
|
||||
if (req.destroyed) return;
|
||||
|
||||
const streams = [res];
|
||||
|
||||
// uncompress the response body transparently if required
|
||||
var responseStream = res;
|
||||
let responseStream = res;
|
||||
|
||||
// return the last request in case of redirects
|
||||
var lastRequest = res.req || req;
|
||||
const lastRequest = res.req || req;
|
||||
|
||||
// if decompress disabled we should not decompress
|
||||
if (config.decompress !== false) {
|
||||
@@ -323,19 +386,48 @@ module.exports = function httpAdapter(config) {
|
||||
case 'compress':
|
||||
case 'deflate':
|
||||
// add the unzipper to the body stream processing pipeline
|
||||
responseStream = responseStream.pipe(zlib.createUnzip());
|
||||
streams.push(zlib.createUnzip());
|
||||
|
||||
// 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());
|
||||
delete res.headers['content-encoding'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var response = {
|
||||
if (onDownloadProgress) {
|
||||
const responseLength = +res.headers['content-length'];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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: res.headers,
|
||||
config: config,
|
||||
headers: new AxiosHeaders(res.headers),
|
||||
config,
|
||||
request: lastRequest
|
||||
};
|
||||
|
||||
@@ -343,8 +435,9 @@ module.exports = function httpAdapter(config) {
|
||||
response.data = responseStream;
|
||||
settle(resolve, reject, response);
|
||||
} else {
|
||||
var responseBuffer = [];
|
||||
var totalResponseBytes = 0;
|
||||
const responseBuffer = [];
|
||||
let totalResponseBytes = 0;
|
||||
|
||||
responseStream.on('data', function handleStreamData(chunk) {
|
||||
responseBuffer.push(chunk);
|
||||
totalResponseBytes += chunk.length;
|
||||
@@ -363,23 +456,25 @@ module.exports = function httpAdapter(config) {
|
||||
if (rejected) {
|
||||
return;
|
||||
}
|
||||
responseStream.destroy();
|
||||
reject(new AxiosError(
|
||||
|
||||
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.aborted) return;
|
||||
if (req.destroyed) return;
|
||||
reject(AxiosError.from(err, null, config, lastRequest));
|
||||
});
|
||||
|
||||
responseStream.on('end', function handleStreamEnd() {
|
||||
try {
|
||||
var responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
|
||||
let responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
|
||||
if (responseType !== 'arraybuffer') {
|
||||
responseData = responseData.toString(responseEncoding);
|
||||
if (!responseEncoding || responseEncoding === 'utf8') {
|
||||
@@ -393,6 +488,18 @@ module.exports = function httpAdapter(config) {
|
||||
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
|
||||
@@ -411,7 +518,7 @@ module.exports = function httpAdapter(config) {
|
||||
// Handle request timeout
|
||||
if (config.timeout) {
|
||||
// This is forcing a int timeout to avoid problems if the `req` interface doesn't handle other types.
|
||||
var timeout = parseInt(config.timeout, 10);
|
||||
const timeout = parseInt(config.timeout, 10);
|
||||
|
||||
if (isNaN(timeout)) {
|
||||
reject(new AxiosError(
|
||||
@@ -430,9 +537,9 @@ module.exports = function httpAdapter(config) {
|
||||
// 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 (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;
|
||||
}
|
||||
@@ -442,33 +549,34 @@ module.exports = function httpAdapter(config) {
|
||||
config,
|
||||
req
|
||||
));
|
||||
abort();
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
}).pipe(req);
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user