From a3f0cfa0940579376341831f347eb5274241d3ba Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Wed, 13 Oct 2021 21:46:05 +0300 Subject: [PATCH] DRAFT --- index.d.ts | 26 +++++++++-- lib/adapters/http.js | 21 ++++----- lib/adapters/xhr.js | 11 ++--- lib/axios.js | 2 +- lib/cancel/CanceledError.js | 4 +- lib/core/AxiosError.js | 62 ++++++++++++++----------- lib/core/dispatchRequest.js | 4 +- lib/core/settle.js | 2 +- lib/defaults.js | 4 +- lib/helpers/validator.js | 12 +++-- lib/utils.js | 44 ++++++++++++++++-- test/specs/cancel/CanceledError.spec.js | 2 +- test/specs/core/AxiosError.spec.js | 10 ++-- test/specs/helpers/isAxiosError.spec.js | 4 +- test/specs/transform.spec.js | 3 ++ test/specs/utils/toFlatObject.js | 10 ++++ test/unit/adapters/http.js | 3 +- 17 files changed, 150 insertions(+), 74 deletions(-) create mode 100644 test/specs/utils/toFlatObject.js diff --git a/index.d.ts b/index.d.ts index 209e0bd857..e61bfb73be 100644 --- a/index.d.ts +++ b/index.d.ts @@ -121,16 +121,34 @@ export interface AxiosResponse { request?: any; } -export interface AxiosError extends Error { +export class AxiosError extends Error { + constructor( + message?: string, + code?: string, + config?: AxiosRequestConfig, + request?: any, + response?: AxiosResponse + ); config: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse; isAxiosError: boolean; + status?: string; toJSON: () => object; -} - -export interface CanceledError extends AxiosError{ + static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS"; + static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE"; + static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION"; + static readonly ERR_NETWORK = "ERR_NETWORK"; + static readonly ERR_DEPRECATED = "ERR_DEPRECATED"; + static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE"; + static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST"; + static readonly ERR_CANCELED = "ERR_CANCELED"; + static readonly ECONNABORTED = "ECONNABORTED"; + static readonly ETIMEDOUT = "ETIMEDOUT"; +} + +export class CanceledError extends AxiosError { } export interface AxiosPromise extends Promise> { diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 776b6f3ac4..f5081fead8 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -11,11 +11,9 @@ var httpsFollow = require('follow-redirects').https; var url = require('url'); var zlib = require('zlib'); var VERSION = require('./../env/data').version; -var createError = require('../core/createError'); -var enhanceError = require('../core/enhanceError'); var defaults = require('../defaults'); -var Cancel = require('../cancel/Cancel'); var AxiosError = require('../core/AxiosError'); +var CanceledError = require('../cancel/CanceledError'); var isHttps = /https:?/; @@ -96,6 +94,7 @@ module.exports = function httpAdapter(config) { } else { return reject(new AxiosError( 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', + AxiosError.ERR_BAD_REQUEST, config )); } @@ -272,13 +271,13 @@ module.exports = function httpAdapter(config) { if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) { stream.destroy(); reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded', - config, null, lastRequest)); + AxiosError.ERR_BAD_RESPONSE, config, lastRequest)); } }); stream.on('error', function handleStreamError(err) { if (req.aborted) return; - reject(AxiosError.from(err, config, null, lastRequest)); + reject(AxiosError.from(err, null, config, lastRequest)); }); stream.on('end', function handleStreamEnd() { @@ -298,8 +297,8 @@ module.exports = function httpAdapter(config) { // Handle errors req.on('error', function handleRequestError(err) { - if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return; - reject(AxiosError.from(err, config, null, req)); + if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return; + reject(AxiosError.from(err, null, config, req)); }); // Handle request timeout @@ -308,10 +307,10 @@ module.exports = function httpAdapter(config) { var timeout = parseInt(config.timeout, 10); if (isNaN(timeout)) { - reject(createError( + reject(new AxiosError( 'error trying to parse `config.timeout` to int', + AxiosError.ERR_BAD_OPTION_VALUE, config, - 'ERR_PARSE_TIMEOUT', req )); @@ -328,8 +327,8 @@ module.exports = function httpAdapter(config) { var transitional = config.transitional || defaults.transitional; reject(new AxiosError( 'timeout of ' + timeout + 'ms exceeded', + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, - transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED', req )); }); @@ -344,7 +343,7 @@ module.exports = function httpAdapter(config) { req.abort(); cancel.config = config; cancel.request = req; - reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel); + reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel); }; config.cancelToken && config.cancelToken.subscribe(onCanceled); diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index 34fbe6927e..cb64fba92a 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -7,10 +7,9 @@ var buildURL = require('./../helpers/buildURL'); var buildFullPath = require('../core/buildFullPath'); var parseHeaders = require('./../helpers/parseHeaders'); var isURLSameOrigin = require('./../helpers/isURLSameOrigin'); -var createError = require('../core/createError'); var defaults = require('../defaults'); -var Cancel = require('../cancel/Cancel'); var AxiosError = require('../core/AxiosError'); +var CanceledError = require('../cancel/CanceledError'); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { @@ -105,7 +104,7 @@ module.exports = function xhrAdapter(config) { return; } - reject(new AxiosError('Request aborted', config, 'ECONNABORTED', request)); + reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request)); // Clean up request request = null; @@ -115,7 +114,7 @@ module.exports = function xhrAdapter(config) { request.onerror = function handleError() { // Real errors are hidden from us by the browser // onerror should only fire if it's a network error - reject(new AxiosError('Network Error', config, null, request)); + reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request, request)); // Clean up request request = null; @@ -130,8 +129,8 @@ module.exports = function xhrAdapter(config) { } reject(new AxiosError( timeoutErrorMessage, + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, - transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED', request)); // Clean up request @@ -192,7 +191,7 @@ module.exports = function xhrAdapter(config) { if (!request) { return; } - reject(!cancel || (cancel && cancel.type) ? new AxiosCancel('canceled') : cancel); + reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel); request.abort(); request = null; }; diff --git a/lib/axios.js b/lib/axios.js index c12beb4cce..30b48093d9 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -37,7 +37,7 @@ var axios = createInstance(defaults); axios.Axios = Axios; // Expose Cancel & CancelToken -axios.Cancel = require('./cancel/Cancel'); +axios.CanceledError = require('./cancel/CanceledError'); axios.CancelToken = require('./cancel/CancelToken'); axios.isCancel = require('./cancel/isCancel'); axios.VERSION = require('./env/data').version; diff --git a/lib/cancel/CanceledError.js b/lib/cancel/CanceledError.js index f141e47e76..b7ceb02567 100644 --- a/lib/cancel/CanceledError.js +++ b/lib/cancel/CanceledError.js @@ -10,7 +10,9 @@ var utils = require('../utils'); * @param {string=} message The message. */ function CanceledError(message) { - AxiosError.call(this, message, null, 'ECONNABORTED'); + // eslint-disable-next-line no-eq-null,eqeqeq + AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED); + this.name = 'CanceledError'; } utils.inherits(CanceledError, AxiosError, { diff --git a/lib/core/AxiosError.js b/lib/core/AxiosError.js index 532ce59c27..52c806a817 100644 --- a/lib/core/AxiosError.js +++ b/lib/core/AxiosError.js @@ -6,20 +6,20 @@ var utils = require('../utils'); * Create an Error with the specified message, config, error code, request and response. * * @param {string} message The error message. - * @param {Object} config The config. - * @param {string?} [code] The error code (for example, 'ECONNABORTED'). + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [config] The config. * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The created error. */ -function AxiosError(message, config, code, request, response) { +function AxiosError(message, code, config, request, response) { Error.call(this); this.message = message; - this.name = this.constructor.name; - this.config = config; + this.name = 'AxiosError'; code && (this.code = code); - this.request = request; - this.response = response; + config && (this.config = config); + request && (this.request = request); + response && (this.response = response); } utils.inherits(AxiosError, Error, { @@ -38,42 +38,48 @@ utils.inherits(AxiosError, Error, { stack: this.stack, // Axios config: this.config, - code: this.code + code: this.code, + status: this.response && this.response.status ? this.response.status : null }; } }); var prototype = AxiosError.prototype; +var descriptors = {}; +[ + 'ERR_BAD_OPTION_VALUE', + 'ERR_BAD_OPTION', + 'ECONNABORTED', + 'ETIMEDOUT', + 'ERR_NETWORK', + 'ERR_FR_TOO_MANY_REDIRECTS', + 'ERR_DEPRECATED', + 'ERR_BAD_RESPONSE', + 'ERR_BAD_REQUEST', + 'ERR_CANCELED' +// eslint-disable-next-line func-names +].forEach(function(code) { + descriptors[code] = {value: code}; +}); + +Object.defineProperties(AxiosError, descriptors); Object.defineProperty(prototype, 'isAxiosError', {value: true}); // eslint-disable-next-line func-names -AxiosError.from = function(error, config, code, request, response) { +AxiosError.from = function(error, code, config, request, response, customProps) { var axiosError = Object.create(prototype); - var props; - var i; - var prop; - var obj = error; - var merged = {}; + utils.toFlatObject(error, axiosError, function filter(obj) { + return obj !== Error.prototype; + }); - do { - props = Object.getOwnPropertyNames(obj); - i = props.length; - while (i-- > 0) { - prop = props[i]; - if (!merged[prop]) { - axiosError[prop] = obj[prop]; - merged[prop] = true; - } - } - obj = Object.getPrototypeOf(obj); - } while (obj && obj !== Error.prototype && obj !== Object.prototype); - - AxiosError.call(axiosError, error.message, config, code, request, response); + AxiosError.call(axiosError, error.message, code, config, request, response); axiosError.name = error.name; + customProps && Object.assign(axiosError, customProps); + return axiosError; }; diff --git a/lib/core/dispatchRequest.js b/lib/core/dispatchRequest.js index 80a8980477..fa1ad95765 100644 --- a/lib/core/dispatchRequest.js +++ b/lib/core/dispatchRequest.js @@ -4,7 +4,7 @@ var utils = require('./../utils'); var transformData = require('./transformData'); var isCancel = require('../cancel/isCancel'); var defaults = require('../defaults'); -var Cancel = require('../cancel/Cancel'); +var CanceledError = require('../cancel/CanceledError'); /** * Throws a `CanceledError` if cancellation has been requested. @@ -15,7 +15,7 @@ function throwIfCancellationRequested(config) { } if (config.signal && config.signal.aborted) { - throw new Cancel('canceled'); + throw new CanceledError(); } } diff --git a/lib/core/settle.js b/lib/core/settle.js index de3278886f..bbb9afa8cb 100644 --- a/lib/core/settle.js +++ b/lib/core/settle.js @@ -16,8 +16,8 @@ module.exports = function settle(resolve, reject, response) { } else { reject(new AxiosError( 'Request failed with status code ' + response.status, + [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4], response.config, - null, response.request, response )); diff --git a/lib/defaults.js b/lib/defaults.js index eaee1898a0..33877e57ce 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -2,7 +2,7 @@ var utils = require('./utils'); var normalizeHeaderName = require('./helpers/normalizeHeaderName'); -var enhanceError = require('./core/enhanceError'); +var AxiosError = require('./core/AxiosError'); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -90,7 +90,7 @@ var defaults = { } catch (e) { if (strictJSONParsing) { if (e.name === 'SyntaxError') { - throw enhanceError(e, this, 'E_JSON_PARSE'); + throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response); } throw e; } diff --git a/lib/helpers/validator.js b/lib/helpers/validator.js index a4ec4133e0..8095b90e10 100644 --- a/lib/helpers/validator.js +++ b/lib/helpers/validator.js @@ -1,6 +1,7 @@ 'use strict'; var VERSION = require('../env/data').version; +var AxiosError = require('../core/AxiosError'); var validators = {}; @@ -28,7 +29,10 @@ validators.transitional = function transitional(validator, version, message) { // eslint-disable-next-line func-names return function(value, opt, opts) { if (validator === false) { - throw new Error(formatMessage(opt, ' has been removed' + (version ? ' in ' + version : ''))); + throw new AxiosError( + formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')), + AxiosError.ERR_DEPRECATED + ); } if (version && !deprecatedWarnings[opt]) { @@ -55,7 +59,7 @@ validators.transitional = function transitional(validator, version, message) { function assertOptions(options, schema, allowUnknown) { if (typeof options !== 'object') { - throw new TypeError('options must be an object'); + throw new AxiosError('options must be an object', AxiosError.ERR_BAD_OPTION_VALUE); } var keys = Object.keys(options); var i = keys.length; @@ -66,12 +70,12 @@ function assertOptions(options, schema, allowUnknown) { var value = options[opt]; var result = value === undefined || validator(value, opt, options); if (result !== true) { - throw new TypeError('option ' + opt + ' must be ' + result); + throw new AxiosError('option ' + opt + ' must be ' + result, AxiosError.ERR_BAD_OPTION_VALUE); } continue; } if (allowUnknown !== true) { - throw Error('Unknown option ' + opt); + throw new AxiosError('Unknown option ' + opt, AxiosError.ERR_BAD_OPTION); } } } diff --git a/lib/utils.js b/lib/utils.js index 28210cc8d4..140a37e4a7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -327,13 +327,46 @@ function stripBOM(content) { * Inherit the prototype methods from one constructor into another * @param {function} constructor * @param {function} superConstructor - * @param {object=} proto + * @param {object} [props] + * @param {object} [descriptors] */ -function inherits(constructor, superConstructor, proto) { - constructor.prototype = Object.create(superConstructor.prototype); +function inherits(constructor, superConstructor, props, descriptors) { + constructor.prototype = Object.create(superConstructor.prototype, descriptors); constructor.prototype.constructor = constructor; - proto && Object.assign(constructor.prototype, proto); + props && Object.assign(constructor.prototype, props); +} + +/** + * Resolve object with deep prototype chain to a flat object + * @param {Object} sourceObj source object + * @param {Object} [destObj] + * @param {Function} [filter] + * @returns {Object} + */ + +function toFlatObject(sourceObj, destObj, filter) { + var props; + var i; + var prop; + var merged = {}; + + destObj = destObj || {}; + + do { + props = Object.getOwnPropertyNames(sourceObj); + i = props.length; + while (i-- > 0) { + prop = props[i]; + if (!merged[prop]) { + destObj[prop] = sourceObj[prop]; + merged[prop] = true; + } + } + sourceObj = Object.getPrototypeOf(sourceObj); + } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype); + + return destObj; } module.exports = { @@ -359,5 +392,6 @@ module.exports = { extend: extend, trim: trim, stripBOM: stripBOM, - inherits: inherits + inherits: inherits, + toFlatObject: toFlatObject }; diff --git a/test/specs/cancel/CanceledError.spec.js b/test/specs/cancel/CanceledError.spec.js index 04709e9f71..de996d581a 100644 --- a/test/specs/cancel/CanceledError.spec.js +++ b/test/specs/cancel/CanceledError.spec.js @@ -4,7 +4,7 @@ describe('Cancel', function() { describe('toString', function() { it('returns correct result when message is not specified', function() { var cancel = new CanceledError(); - expect(cancel.toString()).toBe('CanceledError'); + expect(cancel.toString()).toBe('CanceledError: canceled'); }); it('returns correct result when message is specified', function() { diff --git a/test/specs/core/AxiosError.spec.js b/test/specs/core/AxiosError.spec.js index aa7e999b97..49a9a6fb0d 100644 --- a/test/specs/core/AxiosError.spec.js +++ b/test/specs/core/AxiosError.spec.js @@ -1,10 +1,10 @@ var AxiosError = require('../../../lib/core/AxiosError'); -describe('core::createError', function() { +describe('core::AxiosError', function() { it('should create an Error with message, config, code, request, response and isAxiosError', function() { var request = { path: '/foo' }; var response = { status: 200, data: { foo: 'bar' } }; - var error = new AxiosError('Boom!', { foo: 'bar' }, 'ESOMETHING', request, response); + var error = new AxiosError('Boom!', 'ESOMETHING', { foo: 'bar' }, request, response); expect(error instanceof Error).toBe(true); expect(error.message).toBe('Boom!'); expect(error.config).toEqual({ foo: 'bar' }); @@ -18,7 +18,7 @@ describe('core::createError', function() { // TypeError: Converting circular structure to JSON var request = { path: '/foo' }; var response = { status: 200, data: { foo: 'bar' } }; - var error = new AxiosError('Boom!', { foo: 'bar' }, 'ESOMETHING', request, response); + var error = new AxiosError('Boom!', 'ESOMETHING', { foo: 'bar' }, request, response); var json = error.toJSON(); expect(json.message).toBe('Boom!'); expect(json.config).toEqual({ foo: 'bar' }); @@ -34,7 +34,7 @@ describe('core::createError', function() { var request = { path: '/foo' }; var response = { status: 200, data: { foo: 'bar' } }; - var axiosError = AxiosError.from(error, { foo: 'bar' }, 'ESOMETHING', request, response); + var axiosError = AxiosError.from(error, 'ESOMETHING', { foo: 'bar' }, request, response); expect(axiosError.config).toEqual({ foo: 'bar' }); expect(axiosError.code).toBe('ESOMETHING'); expect(axiosError.request).toBe(request); @@ -44,7 +44,7 @@ describe('core::createError', function() { it('should return error', function() { var error = new Error('Boom!'); - expect(AxiosError.from(error, { foo: 'bar' }, 'ESOMETHING') instanceof AxiosError).toBeTruthy(); + expect(AxiosError.from(error, 'ESOMETHING', { foo: 'bar' }) instanceof AxiosError).toBeTruthy(); }); }); }); diff --git a/test/specs/helpers/isAxiosError.spec.js b/test/specs/helpers/isAxiosError.spec.js index 155c3a2645..1f8b3a15db 100644 --- a/test/specs/helpers/isAxiosError.spec.js +++ b/test/specs/helpers/isAxiosError.spec.js @@ -3,12 +3,12 @@ var isAxiosError = require('../../../lib/helpers/isAxiosError'); describe('helpers::isAxiosError', function() { it('should return true if the error is created by core::createError', function() { - expect(isAxiosError(new AxiosError('Boom!', { foo: 'bar' }))) + expect(isAxiosError(new AxiosError('Boom!', null, { foo: 'bar' }))) .toBe(true); }); it('should return true if the error is enhanced by core::enhanceError', function() { - expect(isAxiosError(AxiosError.from(new Error('Boom!'), { foo: 'bar' }))) + expect(isAxiosError(AxiosError.from(new Error('Boom!'), null, { foo: 'bar' }))) .toBe(true); }); diff --git a/test/specs/transform.spec.js b/test/specs/transform.spec.js index 6f81f46944..f7e62cb957 100644 --- a/test/specs/transform.spec.js +++ b/test/specs/transform.spec.js @@ -1,3 +1,5 @@ +var AxiosError = require("../../lib/core/AxiosError"); + describe('transform', function () { beforeEach(function () { jasmine.Ajax.install(); @@ -64,6 +66,7 @@ describe('transform', function () { setTimeout(function () { expect(thrown).toBeTruthy(); expect(thrown.name).toContain('SyntaxError'); + expect(thrown.code).toEqual(AxiosError.ERR_BAD_RESPONSE); done(); }, 100); }); diff --git a/test/specs/utils/toFlatObject.js b/test/specs/utils/toFlatObject.js new file mode 100644 index 0000000000..90b04d017b --- /dev/null +++ b/test/specs/utils/toFlatObject.js @@ -0,0 +1,10 @@ +var toFlatObject = require('../../../lib/utils').toFlatObject; + +describe('utils::toFlatObject', function () { + it('should resolve object proto chain to a flat object representation', function () { + var a = {x: 1}; + var b = Object.create(a, {y: {value: 2}}); + var c = Object.create(b, {z: {value: 3}}); + expect(toFlatObject(c)).toEqual({x: 1, y: 2, z: 3}); + }); +}); diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index f886926c60..0a1bab8a61 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -9,6 +9,7 @@ var fs = require('fs'); var path = require('path'); var pkg = require('./../../../package.json'); var server, proxy; +var AxiosError = require('../../../lib/core/AxiosError'); describe('supports http with nodejs', function () { @@ -51,7 +52,7 @@ describe('supports http with nodejs', function () { setTimeout(function () { assert.equal(success, false, 'request should not succeed'); assert.equal(failure, true, 'request should fail'); - assert.equal(error.code, 'ERR_PARSE_TIMEOUT'); + assert.equal(error.code, AxiosError.ERR_BAD_OPTION_VALUE); assert.equal(error.message, 'error trying to parse `config.timeout` to int'); done(); }, 300);