From e7f33e69be4cc931ff85b77050b672ac55c72e7e Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Thu, 14 Oct 2021 18:53:46 +0300 Subject: [PATCH] Refactored AxiosError to a constructor; Refactored `Cancel` to a constructor, a subclass of the `AxiosError`; Expose CanceledError class; Refactored axios error codes; Added `toFlatObject` util; --- .eslintrc.js | 245 ++++++++++++------------ .github/workflows/ci.yml | 6 +- CHANGELOG.md | 61 ++++++ Gruntfile.js | 40 ++-- README.md | 22 ++- bower.json | 2 +- index.d.ts | 118 ++++++++---- lib/adapters/http.js | 82 +++++--- lib/adapters/xhr.js | 49 +++-- lib/axios.js | 9 +- lib/cancel/Cancel.js | 19 -- lib/cancel/CancelToken.js | 68 ++++++- lib/cancel/CanceledError.js | 22 +++ lib/core/Axios.js | 6 +- lib/core/AxiosError.js | 86 +++++++++ lib/core/createError.js | 18 -- lib/core/dispatchRequest.js | 7 +- lib/core/enhanceError.js | 43 ----- lib/core/mergeConfig.js | 88 +++++---- lib/core/settle.js | 6 +- lib/defaults.js | 33 +++- lib/env/README.md | 3 + lib/env/data.js | 3 + lib/helpers/validator.js | 47 ++--- lib/utils.js | 50 ++++- package.json | 13 +- test/specs/api.spec.js | 2 +- test/specs/cancel.spec.js | 51 +++-- test/specs/cancel/Cancel.spec.js | 15 -- test/specs/cancel/CancelToken.spec.js | 16 +- test/specs/cancel/CanceledError.spec.js | 15 ++ test/specs/cancel/isCancel.spec.js | 8 +- test/specs/core/AxiosError.spec.js | 50 +++++ test/specs/core/createError.spec.js | 30 --- test/specs/core/enhanceError.spec.js | 31 --- test/specs/defaults.spec.js | 13 ++ test/specs/helpers/isAxiosError.spec.js | 15 +- test/specs/helpers/validator.spec.js | 21 +- test/specs/instance.spec.js | 3 + test/specs/transform.spec.js | 3 + test/specs/utils/toFlatObject.js | 10 + test/typescript/axios.ts | 10 +- test/unit/adapters/http.js | 44 ++++- tsconfig.json | 14 ++ tslint.json | 6 + webpack.config.js | 3 +- 46 files changed, 974 insertions(+), 532 deletions(-) delete mode 100644 lib/cancel/Cancel.js create mode 100644 lib/cancel/CanceledError.js create mode 100644 lib/core/AxiosError.js delete mode 100644 lib/core/createError.js create mode 100644 lib/env/README.md create mode 100644 lib/env/data.js delete mode 100644 test/specs/cancel/Cancel.spec.js create mode 100644 test/specs/cancel/CanceledError.spec.js create mode 100644 test/specs/core/AxiosError.spec.js delete mode 100644 test/specs/core/createError.spec.js create mode 100644 test/specs/utils/toFlatObject.js create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.eslintrc.js b/.eslintrc.js index 7f9e73ed1c..d3bfa24c05 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,148 +1,151 @@ +// eslint-disable-next-line strict module.exports = { - "globals": { - "console": true, - "module": true, - "require": true + 'globals': { + 'console': true, + 'module': true, + 'require': true }, - "env": { - "browser": true, - "node": true + 'env': { + 'browser': true, + 'node': true }, - "rules": { -/** + 'rules': { + /** * Strict mode */ - "strict": [2, "global"], // http://eslint.org/docs/rules/strict + 'strict': [2, 'global'], // http://eslint.org/docs/rules/strict -/** + /** * Variables */ - "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow - "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names - "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars - "vars": "local", - "args": "after-used" + 'no-shadow': 2, // http://eslint.org/docs/rules/no-shadow + 'no-shadow-restricted-names': 2, // http://eslint.org/docs/rules/no-shadow-restricted-names + 'no-unused-vars': [2, { // http://eslint.org/docs/rules/no-unused-vars + 'vars': 'local', + 'args': 'after-used' }], - "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define + 'no-use-before-define': 2, // http://eslint.org/docs/rules/no-use-before-define -/** + /** * Possible errors */ - "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle - "no-cond-assign": [2, "except-parens"], // http://eslint.org/docs/rules/no-cond-assign - "no-console": 1, // http://eslint.org/docs/rules/no-console - "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger - "no-alert": 1, // http://eslint.org/docs/rules/no-alert - "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition - "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys - "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case - "no-empty": 2, // http://eslint.org/docs/rules/no-empty - "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign - "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast - "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi - "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign - "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations - "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp - "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace - "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls - "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays - "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable - "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan - "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var + 'comma-dangle': [2, 'never'], // http://eslint.org/docs/rules/comma-dangle + 'no-cond-assign': [2, 'except-parens'], // http://eslint.org/docs/rules/no-cond-assign + 'no-console': 1, // http://eslint.org/docs/rules/no-console + 'no-debugger': 1, // http://eslint.org/docs/rules/no-debugger + 'no-alert': 1, // http://eslint.org/docs/rules/no-alert + 'no-constant-condition': 1, // http://eslint.org/docs/rules/no-constant-condition + 'no-dupe-keys': 2, // http://eslint.org/docs/rules/no-dupe-keys + 'no-duplicate-case': 2, // http://eslint.org/docs/rules/no-duplicate-case + 'no-empty': 2, // http://eslint.org/docs/rules/no-empty + 'no-ex-assign': 2, // http://eslint.org/docs/rules/no-ex-assign + 'no-extra-boolean-cast': 0, // http://eslint.org/docs/rules/no-extra-boolean-cast + 'no-extra-semi': 2, // http://eslint.org/docs/rules/no-extra-semi + 'no-func-assign': 2, // http://eslint.org/docs/rules/no-func-assign + 'no-inner-declarations': 2, // http://eslint.org/docs/rules/no-inner-declarations + 'no-invalid-regexp': 2, // http://eslint.org/docs/rules/no-invalid-regexp + 'no-irregular-whitespace': 2, // http://eslint.org/docs/rules/no-irregular-whitespace + 'no-obj-calls': 2, // http://eslint.org/docs/rules/no-obj-calls + 'no-sparse-arrays': 2, // http://eslint.org/docs/rules/no-sparse-arrays + 'no-unreachable': 2, // http://eslint.org/docs/rules/no-unreachable + 'use-isnan': 2, // http://eslint.org/docs/rules/use-isnan + 'block-scoped-var': 2, // http://eslint.org/docs/rules/block-scoped-var -/** + /** * Best practices */ - "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return - "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly - "default-case": 2, // http://eslint.org/docs/rules/default-case - "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation - "allowKeywords": true + 'consistent-return': 2, // http://eslint.org/docs/rules/consistent-return + 'curly': [2, 'multi-line'], // http://eslint.org/docs/rules/curly + 'default-case': 2, // http://eslint.org/docs/rules/default-case + 'dot-notation': [2, { // http://eslint.org/docs/rules/dot-notation + 'allowKeywords': true }], - "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq - "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in - "no-caller": 2, // http://eslint.org/docs/rules/no-caller - "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return - "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null - "no-eval": 2, // http://eslint.org/docs/rules/no-eval - "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native - "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind - "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough - "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal - "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval - "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks - "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func - "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str - "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign - "no-new": 2, // http://eslint.org/docs/rules/no-new - "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func - "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers - "no-octal": 2, // http://eslint.org/docs/rules/no-octal - "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape - "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign - "no-proto": 2, // http://eslint.org/docs/rules/no-proto - "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare - "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign - "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url - "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare - "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences - "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal - "no-with": 2, // http://eslint.org/docs/rules/no-with - "radix": 2, // http://eslint.org/docs/rules/radix - "vars-on-top": 0, // http://eslint.org/docs/rules/vars-on-top - "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife - "yoda": 2, // http://eslint.org/docs/rules/yoda + 'eqeqeq': 2, // http://eslint.org/docs/rules/eqeqeq + 'guard-for-in': 2, // http://eslint.org/docs/rules/guard-for-in + 'no-caller': 2, // http://eslint.org/docs/rules/no-caller + 'no-else-return': 2, // http://eslint.org/docs/rules/no-else-return + 'no-eq-null': 2, // http://eslint.org/docs/rules/no-eq-null + 'no-eval': 2, // http://eslint.org/docs/rules/no-eval + 'no-extend-native': 2, // http://eslint.org/docs/rules/no-extend-native + 'no-extra-bind': 2, // http://eslint.org/docs/rules/no-extra-bind + 'no-fallthrough': 2, // http://eslint.org/docs/rules/no-fallthrough + 'no-floating-decimal': 2, // http://eslint.org/docs/rules/no-floating-decimal + 'no-implied-eval': 2, // http://eslint.org/docs/rules/no-implied-eval + 'no-lone-blocks': 2, // http://eslint.org/docs/rules/no-lone-blocks + 'no-loop-func': 2, // http://eslint.org/docs/rules/no-loop-func + 'no-multi-str': 2, // http://eslint.org/docs/rules/no-multi-str + 'no-native-reassign': 2, // http://eslint.org/docs/rules/no-native-reassign + 'no-new': 2, // http://eslint.org/docs/rules/no-new + 'no-new-func': 2, // http://eslint.org/docs/rules/no-new-func + 'no-new-wrappers': 2, // http://eslint.org/docs/rules/no-new-wrappers + 'no-octal': 2, // http://eslint.org/docs/rules/no-octal + 'no-octal-escape': 2, // http://eslint.org/docs/rules/no-octal-escape + 'no-param-reassign': 2, // http://eslint.org/docs/rules/no-param-reassign + 'no-proto': 2, // http://eslint.org/docs/rules/no-proto + 'no-redeclare': 2, // http://eslint.org/docs/rules/no-redeclare + 'no-return-assign': 2, // http://eslint.org/docs/rules/no-return-assign + 'no-script-url': 2, // http://eslint.org/docs/rules/no-script-url + 'no-self-compare': 2, // http://eslint.org/docs/rules/no-self-compare + 'no-sequences': 2, // http://eslint.org/docs/rules/no-sequences + 'no-throw-literal': 2, // http://eslint.org/docs/rules/no-throw-literal + 'no-with': 2, // http://eslint.org/docs/rules/no-with + 'radix': 2, // http://eslint.org/docs/rules/radix + 'vars-on-top': 0, // http://eslint.org/docs/rules/vars-on-top + 'wrap-iife': [2, 'any'], // http://eslint.org/docs/rules/wrap-iife + 'yoda': 2, // http://eslint.org/docs/rules/yoda -/** + /** * Style */ - "indent": [2, 2], // http://eslint.org/docs/rules/indent - "brace-style": [2, // http://eslint.org/docs/rules/brace-style - "1tbs", { - "allowSingleLine": true - }], - "quotes": [ - 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes + 'indent': [2, 2], // http://eslint.org/docs/rules/indent + 'brace-style': [2, // http://eslint.org/docs/rules/brace-style + '1tbs', { + 'allowSingleLine': true + }], + 'quotes': [ + 2, 'single', 'avoid-escape' // http://eslint.org/docs/rules/quotes ], - "camelcase": [2, { // http://eslint.org/docs/rules/camelcase - "properties": "never" + 'camelcase': [2, { // http://eslint.org/docs/rules/camelcase + 'properties': 'never' }], - "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing - "before": false, - "after": true + 'comma-spacing': [2, { // http://eslint.org/docs/rules/comma-spacing + 'before': false, + 'after': true }], - "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style - "eol-last": 2, // http://eslint.org/docs/rules/eol-last - "func-names": 1, // http://eslint.org/docs/rules/func-names - "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing - "beforeColon": false, - "afterColon": true + 'comma-style': [2, 'last'], // http://eslint.org/docs/rules/comma-style + 'eol-last': 2, // http://eslint.org/docs/rules/eol-last + 'func-names': 1, // http://eslint.org/docs/rules/func-names + 'key-spacing': [2, { // http://eslint.org/docs/rules/key-spacing + 'beforeColon': false, + 'afterColon': true }], - "new-cap": [2, { // http://eslint.org/docs/rules/new-cap - "newIsCap": true + 'new-cap': [2, { // http://eslint.org/docs/rules/new-cap + 'newIsCap': true }], - "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines - "max": 2 + 'no-multiple-empty-lines': [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines + 'max': 2 }], - "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary - "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object - "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func - "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces - "no-extra-parens": [2, "functions"], // http://eslint.org/docs/rules/no-extra-parens - "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle - "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var - "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks - "semi": [2, "always"], // http://eslint.org/docs/rules/semi - "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing - "before": false, - "after": true + 'no-nested-ternary': 2, // http://eslint.org/docs/rules/no-nested-ternary + 'no-new-object': 2, // http://eslint.org/docs/rules/no-new-object + 'no-spaced-func': 2, // http://eslint.org/docs/rules/no-spaced-func + 'no-trailing-spaces': 2, // http://eslint.org/docs/rules/no-trailing-spaces + 'no-extra-parens': [2, 'functions'], // http://eslint.org/docs/rules/no-extra-parens + 'no-underscore-dangle': 0, // http://eslint.org/docs/rules/no-underscore-dangle + 'one-var': [2, 'never'], // http://eslint.org/docs/rules/one-var + 'padded-blocks': [2, 'never'], // http://eslint.org/docs/rules/padded-blocks + 'semi': [2, 'always'], // http://eslint.org/docs/rules/semi + 'semi-spacing': [2, { // http://eslint.org/docs/rules/semi-spacing + 'before': false, + 'after': true }], - "keyword-spacing": 2, // http://eslint.org/docs/rules/keyword-spacing - "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks - "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren - "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops - "spaced-comment": [2, "always", {// http://eslint.org/docs/rules/spaced-comment - "markers": ["global", "eslint"] + 'keyword-spacing': 2, // http://eslint.org/docs/rules/keyword-spacing + 'space-before-blocks': 2, // http://eslint.org/docs/rules/space-before-blocks + 'space-before-function-paren': [2, 'never'], // http://eslint.org/docs/rules/space-before-function-paren + 'space-infix-ops': 2, // http://eslint.org/docs/rules/space-infix-ops + 'spaced-comment': [2, 'always', {// http://eslint.org/docs/rules/spaced-comment + 'markers': ['global', 'eslint'] }] - } -} + }, + + 'ignorePatterns': ['**/env/data.js'] +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be8d079534..fe31b6e2fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: ci on: push: - branches: [master] + branches: [master, 'release/*'] pull_request: - branches: [master] + branches: [master, 'release/*'] jobs: build: @@ -19,6 +19,6 @@ jobs: - name: Setup node uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node }} + node-version: ${{ matrix.node-version }} - run: npm install - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index c1994309ae..d5dcde34dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +### 0.23.0 (October 12, 2021) + +Breaking changes: +- Distinguish request and response data types ([#4116](https://github.com/axios/axios/pull/4116)) +- Change never type to unknown ([#4142](https://github.com/axios/axios/pull/4142)) +- Fixed TransitionalOptions typings ([#4147](https://github.com/axios/axios/pull/4147)) + +Fixes and Functionality: +- Adding globalObject: 'this' to webpack config ([#3176](https://github.com/axios/axios/pull/3176)) +- Adding insecureHTTPParser type to AxiosRequestConfig ([#4066](https://github.com/axios/axios/pull/4066)) +- Fix missing semicolon in typings ([#4115](https://github.com/axios/axios/pull/4115)) +- Fix response headers types ([#4136](https://github.com/axios/axios/pull/4136)) + +Internal and Tests: +- Improve timeout error when timeout is browser default ([#3209](https://github.com/axios/axios/pull/3209)) +- Fix node version on CI ([#4069](https://github.com/axios/axios/pull/4069)) +- Added testing to TypeScript portion of project ([#4140](https://github.com/axios/axios/pull/4140)) + +Documentation: +- Rename Angular to AngularJS ([#4114](https://github.com/axios/axios/pull/4114)) + +Huge thanks to everyone who contributed to this release via code (authors listed below) or via reviews and triaging on GitHub: + +- [Jay](mailto:jasonsaayman@gmail.com) +- [Evan-Finkelstein](https://github.com/Evan-Finkelstein) +- [Paweł Szymański](https://github.com/Jezorko) +- [Dobes Vandermeer](https://github.com/dobesv) +- [Claas Augner](https://github.com/caugner) +- [Remco Haszing](https://github.com/remcohaszing) +- [Evgeniy](https://github.com/egmen) +- [Dmitriy Mozgovoy](https://github.com/DigitalBrainJS) + +### 0.22.0 (October 01, 2021) + +Fixes and Functionality: +- Caseless header comparing in HTTP adapter ([#2880](https://github.com/axios/axios/pull/2880)) +- Avoid package.json import fixing issues and warnings related to this ([#4041](https://github.com/axios/axios/pull/4041)), ([#4065](https://github.com/axios/axios/pull/4065)) +- Fixed cancelToken leakage and added AbortController support ([#3305](https://github.com/axios/axios/pull/3305)) +- Updating CI to run on release branches +- Bump follow redirects version +- Fixed default transitional config for custom Axios instance; ([#4052](https://github.com/axios/axios/pull/4052)) + +Huge thanks to everyone who contributed to this release via code (authors listed below) or via reviews and triaging on GitHub: + +- [Jay](mailto:jasonsaayman@gmail.com) +- [Matt R. Wilson](https://github.com/mastermatt) +- [Xianming Zhong](https://github.com/chinesedfan) +- [Dmitriy Mozgovoy](https://github.com/DigitalBrainJS) + +### 0.21.4 (September 6, 2021) + +Fixes and Functionality: +- Fixing JSON transform when data is stringified. Providing backward compatability and complying to the JSON RFC standard ([#4020](https://github.com/axios/axios/pull/4020)) + +Huge thanks to everyone who contributed to this release via code (authors listed below) or via reviews and triaging on GitHub: + +- [Jay](mailto:jasonsaayman@gmail.com) +- [Guillaume Fortaine](https://github.com/gfortaine) +- [Yusuke Kawasaki](https://github.com/kawanet) +- [Dmitriy Mozgovoy](https://github.com/DigitalBrainJS) + ### 0.21.3 (September 4, 2021) Fixes and Functionality: diff --git a/Gruntfile.js b/Gruntfile.js index 3f527456e8..c07e228a67 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line strict module.exports = function(grunt) { require('load-grunt-tasks')(grunt); @@ -11,19 +12,6 @@ module.exports = function(grunt) { dist: 'dist/**' }, - ts: { - test: { - options: { - lib: [ - 'es5', - 'es2015.promise', - 'dom' - ] - }, - src: ['typings/index.d.ts', 'test/typescript/*.ts'] - } - }, - package2bower: { all: { fields: [ @@ -37,6 +25,10 @@ module.exports = function(grunt) { } }, + package2env: { + all: {} + }, + usebanner: { all: { options: { @@ -70,8 +62,8 @@ module.exports = function(grunt) { src: ['test/unit/**/*.js'] }, options: { - timeout: 30000, - }, + timeout: 30000 + } }, watch: { @@ -88,12 +80,12 @@ module.exports = function(grunt) { webpack: require('./webpack.config.js') }); - grunt.registerMultiTask('package2bower', 'Sync package.json to bower.json', function () { + grunt.registerMultiTask('package2bower', 'Sync package.json to bower.json', function() { var npm = grunt.file.readJSON('package.json'); var bower = grunt.file.readJSON('bower.json'); var fields = this.data.fields || []; - for (var i=0, l=fields.length; i Note: you can cancel several requests with the same cancel token. +Axios supports AbortController to abort requests in [`fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#aborting_a_fetch) way: +```js +const controller = new AbortController(); + +axios.get('/foo/bar', { + signal: controller.signal +}).then(function(response) { + //... +}); +// cancel the request +controller.abort() +``` + +> Note: you can cancel several requests with the same cancel token/abort controller. > If a cancellation token is already cancelled at the moment of starting an Axios request, then the request is cancelled immediately, without any attempts to make real request. ## Using application/x-www-form-urlencoded format @@ -869,7 +885,7 @@ You can use Gitpod an online IDE(which is free for Open Source) for contributing ## Credits -axios is heavily inspired by the [$http service](https://docs.angularjs.org/api/ng/service/$http) provided in [Angular](https://angularjs.org/). Ultimately axios is an effort to provide a standalone `$http`-like service for use outside of Angular. +axios is heavily inspired by the [$http service](https://docs.angularjs.org/api/ng/service/$http) provided in [AngularJS](https://angularjs.org/). Ultimately axios is an effort to provide a standalone `$http`-like service for use outside of AngularJS. ## License diff --git a/bower.json b/bower.json index 2abc4049f4..40f2980578 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "axios", "main": "./dist/axios.js", - "version": "0.21.3", + "version": "0.23.0", "homepage": "https://axios-http.com", "authors": [ "Matt Zabriskie" diff --git a/index.d.ts b/index.d.ts index 08a0be388c..e61bfb73be 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,17 @@ -export interface AxiosTransformer { - (data: any, headers?: Record): any; +// TypeScript Version: 3.0 + +export type AxiosRequestHeaders = Record; + +export type AxiosResponseHeaders = Record & { + "set-cookie"?: string[] +}; + +export interface AxiosRequestTransformer { + (data: any, headers?: AxiosRequestHeaders): any; +} + +export interface AxiosResponseTransformer { + (data: any, headers?: AxiosResponseHeaders): any; } export interface AxiosAdapter { @@ -16,7 +28,7 @@ export interface AxiosProxyConfig { port: number; auth?: { username: string; - password:string; + password: string; }; protocol?: string; } @@ -31,7 +43,7 @@ export type Method = | 'patch' | 'PATCH' | 'purge' | 'PURGE' | 'link' | 'LINK' - | 'unlink' | 'UNLINK' + | 'unlink' | 'UNLINK'; export type ResponseType = | 'arraybuffer' @@ -39,24 +51,24 @@ export type ResponseType = | 'document' | 'json' | 'text' - | 'stream' + | 'stream'; -export interface TransitionalOptions{ - silentJSONParsing: boolean; - forcedJSONParsing: boolean; - clarifyTimeoutError: boolean; +export interface TransitionalOptions { + silentJSONParsing?: boolean; + forcedJSONParsing?: boolean; + clarifyTimeoutError?: boolean; } -export interface AxiosRequestConfig { +export interface AxiosRequestConfig { url?: string; method?: Method; baseURL?: string; - transformRequest?: AxiosTransformer | AxiosTransformer[]; - transformResponse?: AxiosTransformer | AxiosTransformer[]; - headers?: Record; + transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[]; + transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[]; + headers?: AxiosRequestHeaders; params?: any; paramsSerializer?: (params: any) => string; - data?: T; + data?: D; timeout?: number; timeoutErrorMessage?: string; withCredentials?: boolean; @@ -77,28 +89,69 @@ export interface AxiosRequestConfig { proxy?: AxiosProxyConfig | false; cancelToken?: CancelToken; decompress?: boolean; - transitional?: TransitionalOptions + transitional?: TransitionalOptions; + signal?: AbortSignal; + insecureHTTPParser?: boolean; } -export interface AxiosResponse { +export interface HeadersDefaults { + common: AxiosRequestHeaders; + delete: AxiosRequestHeaders; + get: AxiosRequestHeaders; + head: AxiosRequestHeaders; + post: AxiosRequestHeaders; + put: AxiosRequestHeaders; + patch: AxiosRequestHeaders; + options?: AxiosRequestHeaders; + purge?: AxiosRequestHeaders; + link?: AxiosRequestHeaders; + unlink?: AxiosRequestHeaders; +} + +export interface AxiosDefaults extends Omit, 'headers'> { + headers: HeadersDefaults; +} + +export interface AxiosResponse { data: T; status: number; statusText: string; - headers: Record; - config: AxiosRequestConfig; + headers: AxiosResponseHeaders; + config: AxiosRequestConfig; request?: any; } -export interface AxiosError extends Error { - config: AxiosRequestConfig; +export class AxiosError extends Error { + constructor( + message?: string, + code?: string, + config?: AxiosRequestConfig, + request?: any, + response?: AxiosResponse + ); + config: AxiosRequestConfig; code?: string; request?: any; - response?: AxiosResponse; + response?: AxiosResponse; isAxiosError: boolean; + status?: string; toJSON: () => object; + 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> { +export interface AxiosPromise extends Promise> { } export interface CancelStatic { @@ -136,20 +189,20 @@ export interface AxiosInterceptorManager { export class Axios { constructor(config?: AxiosRequestConfig); - defaults: AxiosRequestConfig; + defaults: AxiosDefaults; interceptors: { request: AxiosInterceptorManager; response: AxiosInterceptorManager; }; getUri(config?: AxiosRequestConfig): string; - request> (config: AxiosRequestConfig): Promise; - get>(url: string, config?: AxiosRequestConfig): Promise; - delete>(url: string, config?: AxiosRequestConfig): Promise; - head>(url: string, config?: AxiosRequestConfig): Promise; - options>(url: string, config?: AxiosRequestConfig): Promise; - post>(url: string, data?: T, config?: AxiosRequestConfig): Promise; - put>(url: string, data?: T, config?: AxiosRequestConfig): Promise; - patch>(url: string, data?: T, config?: AxiosRequestConfig): Promise; + request, D = any>(config: AxiosRequestConfig): Promise; + get, D = any>(url: string, config?: AxiosRequestConfig): Promise; + delete, D = any>(url: string, config?: AxiosRequestConfig): Promise; + head, D = any>(url: string, config?: AxiosRequestConfig): Promise; + options, D = any>(url: string, config?: AxiosRequestConfig): Promise; + post, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise; + put, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise; + patch, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise; } export interface AxiosInstance extends Axios { @@ -162,8 +215,9 @@ export interface AxiosStatic extends AxiosInstance { Cancel: CancelStatic; CancelToken: CancelTokenStatic; Axios: typeof Axios; + readonly VERSION: string; isCancel(value: any): boolean; - all(values: (T | Promise)[]): Promise; + all(values: Array>): Promise; spread(callback: (...args: T[]) => R): (array: T[]) => R; isAxiosError(payload: any): payload is AxiosError; } diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 1b76f5e0ae..f5081fead8 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -10,9 +10,10 @@ var httpFollow = require('follow-redirects').http; var httpsFollow = require('follow-redirects').https; var url = require('url'); var zlib = require('zlib'); -var pkg = require('./../../package.json'); -var createError = require('../core/createError'); -var enhanceError = require('../core/enhanceError'); +var VERSION = require('./../env/data').version; +var defaults = require('../defaults'); +var AxiosError = require('../core/AxiosError'); +var CanceledError = require('../cancel/CanceledError'); var isHttps = /https:?/; @@ -44,27 +45,43 @@ function setProxy(options, proxy, location) { /*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 reject = function reject(value) { + done(); rejectPromise(value); }; var data = config.data; 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 headers || 'user-agent' in headers) { + if ('user-agent' in headerNames) { // User-Agent is specified; handle case where no UA header is desired - if (!headers['User-Agent'] && !headers['user-agent']) { - delete headers['User-Agent']; - delete headers['user-agent']; + 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/' + pkg.version; + headers['User-Agent'] = 'axios/' + VERSION; } if (data && !utils.isStream(data)) { @@ -75,14 +92,17 @@ module.exports = function httpAdapter(config) { } else if (utils.isString(data)) { data = Buffer.from(data, 'utf-8'); } else { - return reject(createError( + 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['Content-Length'] = data.length; + if (!headerNames['content-length']) { + headers['Content-Length'] = data.length; + } } // HTTP basic authentication @@ -105,8 +125,8 @@ module.exports = function httpAdapter(config) { auth = urlUsername + ':' + urlPassword; } - if (auth) { - delete headers.Authorization; + if (auth && headerNames.authorization) { + delete headers[headerNames.authorization]; } var isHttpsRequest = isHttps.test(protocol); @@ -250,14 +270,14 @@ module.exports = function httpAdapter(config) { // make sure the content length is not over the maxContentLength if specified if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) { stream.destroy(); - reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', - config, null, lastRequest)); + reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, config, lastRequest)); } }); stream.on('error', function handleStreamError(err) { if (req.aborted) return; - reject(enhanceError(err, config, null, lastRequest)); + reject(AxiosError.from(err, null, config, lastRequest)); }); stream.on('end', function handleStreamEnd() { @@ -277,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(enhanceError(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 @@ -287,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 )); @@ -304,29 +324,39 @@ module.exports = function httpAdapter(config) { // 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(); - reject(createError( + var transitional = config.transitional || defaults.transitional; + reject(new AxiosError( 'timeout of ' + timeout + 'ms exceeded', + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, - config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED', req )); }); } - if (config.cancelToken) { + if (config.cancelToken || config.signal) { // Handle cancellation - config.cancelToken.promise.then(function onCanceled(cancel) { + // eslint-disable-next-line func-names + onCanceled = function(cancel) { if (req.aborted) return; req.abort(); - reject(cancel); - }); + cancel.config = config; + cancel.request = req; + reject(!cancel || (cancel && cancel.type) ? new CanceledError() : 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(enhanceError(err, config, null, req)); + reject(AxiosError.from(err, config, null, req)); }).pipe(req); } else { req.end(data); diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index a386dd24aa..cb64fba92a 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -7,13 +7,25 @@ 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 AxiosError = require('../core/AxiosError'); +var CanceledError = require('../cancel/CanceledError'); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; var requestHeaders = config.headers; var responseType = config.responseType; + var onCanceled; + function done() { + if (config.cancelToken) { + config.cancelToken.unsubscribe(onCanceled); + } + + if (config.signal) { + config.signal.removeEventListener('abort', onCanceled); + } + } if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; // Let the browser set it @@ -51,7 +63,13 @@ module.exports = function xhrAdapter(config) { request: request }; - settle(resolve, reject, response); + settle(function _resolve(value) { + resolve(value); + done(); + }, function _reject(err) { + reject(err); + done(); + }, response); // Clean up request request = null; @@ -86,7 +104,7 @@ module.exports = function xhrAdapter(config) { return; } - reject(createError('Request aborted', config, 'ECONNABORTED', request)); + reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request)); // Clean up request request = null; @@ -96,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(createError('Network Error', config, null, request)); + reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request, request)); // Clean up request request = null; @@ -104,14 +122,15 @@ module.exports = function xhrAdapter(config) { // Handle timeout request.ontimeout = function handleTimeout() { - var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; + var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; + var transitional = config.transitional || defaults.transitional; if (config.timeoutErrorMessage) { timeoutErrorMessage = config.timeoutErrorMessage; } - reject(createError( + reject(new AxiosError( timeoutErrorMessage, + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, - config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED', request)); // Clean up request @@ -165,18 +184,22 @@ module.exports = function xhrAdapter(config) { request.upload.addEventListener('progress', config.onUploadProgress); } - if (config.cancelToken) { + if (config.cancelToken || config.signal) { // Handle cancellation - config.cancelToken.promise.then(function onCanceled(cancel) { + // eslint-disable-next-line func-names + onCanceled = function(cancel) { if (!request) { return; } - + reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel); request.abort(); - reject(cancel); - // Clean up request request = null; - }); + }; + + config.cancelToken && config.cancelToken.subscribe(onCanceled); + if (config.signal) { + config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled); + } } if (!requestData) { diff --git a/lib/axios.js b/lib/axios.js index 0ca8d6b502..30b48093d9 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -37,9 +37,16 @@ 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; + +// Expose AxiosError class +axios.AxiosError = require('../lib/core/AxiosError'); + +// alias for CanceledError for backward compatibility +axios.Cancel = axios.CanceledError; // Expose all/spread axios.all = function all(promises) { diff --git a/lib/cancel/Cancel.js b/lib/cancel/Cancel.js deleted file mode 100644 index e0de4003f9..0000000000 --- a/lib/cancel/Cancel.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -/** - * A `Cancel` is an object that is thrown when an operation is canceled. - * - * @class - * @param {string=} message The message. - */ -function Cancel(message) { - this.message = message; -} - -Cancel.prototype.toString = function toString() { - return 'Cancel' + (this.message ? ': ' + this.message : ''); -}; - -Cancel.prototype.__CANCEL__ = true; - -module.exports = Cancel; diff --git a/lib/cancel/CancelToken.js b/lib/cancel/CancelToken.js index 6b46e66625..ee7989f918 100644 --- a/lib/cancel/CancelToken.js +++ b/lib/cancel/CancelToken.js @@ -1,6 +1,6 @@ 'use strict'; -var Cancel = require('./Cancel'); +var CanceledError = require('./CanceledError'); /** * A `CancelToken` is an object that can be used to request cancellation of an operation. @@ -14,24 +14,55 @@ function CancelToken(executor) { } var resolvePromise; + this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; + + // eslint-disable-next-line func-names + this.promise.then(function(cancel) { + if (!token._listeners) return; + + var i; + var l = token._listeners.length; + + for (i = 0; i < l; i++) { + token._listeners[i](cancel); + } + token._listeners = null; + }); + + // eslint-disable-next-line func-names + this.promise.then = function(onfulfilled) { + var _resolve; + // eslint-disable-next-line func-names + var promise = new Promise(function(resolve) { + token.subscribe(resolve); + _resolve = resolve; + }).then(onfulfilled); + + promise.cancel = function reject() { + token.unsubscribe(_resolve); + }; + + return promise; + }; + executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } - token.reason = new Cancel(message); + token.reason = new CanceledError(message); resolvePromise(token.reason); }); } /** - * Throws a `Cancel` if cancellation has been requested. + * Throws a `CanceledError` if cancellation has been requested. */ CancelToken.prototype.throwIfRequested = function throwIfRequested() { if (this.reason) { @@ -39,6 +70,37 @@ CancelToken.prototype.throwIfRequested = function throwIfRequested() { } }; +/** + * Subscribe to the cancel signal + */ + +CancelToken.prototype.subscribe = function subscribe(listener) { + if (this.reason) { + listener(this.reason); + return; + } + + if (this._listeners) { + this._listeners.push(listener); + } else { + this._listeners = [listener]; + } +}; + +/** + * Unsubscribe from the cancel signal + */ + +CancelToken.prototype.unsubscribe = function unsubscribe(listener) { + if (!this._listeners) { + return; + } + var index = this._listeners.indexOf(listener); + if (index !== -1) { + this._listeners.splice(index, 1); + } +}; + /** * Returns an object that contains a new `CancelToken` and a function that, when called, * cancels the `CancelToken`. diff --git a/lib/cancel/CanceledError.js b/lib/cancel/CanceledError.js new file mode 100644 index 0000000000..b7ceb02567 --- /dev/null +++ b/lib/cancel/CanceledError.js @@ -0,0 +1,22 @@ +'use strict'; + +var AxiosError = require('../core/AxiosError'); +var utils = require('../utils'); + +/** + * A `CanceledError` is an object that is thrown when an operation is canceled. + * + * @class + * @param {string=} message The message. + */ +function CanceledError(message) { + // 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, { + __CANCEL__: true +}); + +module.exports = CanceledError; diff --git a/lib/core/Axios.js b/lib/core/Axios.js index 42ea75e7eb..613af54d42 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -51,9 +51,9 @@ Axios.prototype.request = function request(config) { if (transitional !== undefined) { validator.assertOptions(transitional, { - silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'), - forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'), - clarifyTimeoutError: validators.transitional(validators.boolean, '1.0.0') + silentJSONParsing: validators.transitional(validators.boolean), + forcedJSONParsing: validators.transitional(validators.boolean), + clarifyTimeoutError: validators.transitional(validators.boolean) }, false); } diff --git a/lib/core/AxiosError.js b/lib/core/AxiosError.js new file mode 100644 index 0000000000..52c806a817 --- /dev/null +++ b/lib/core/AxiosError.js @@ -0,0 +1,86 @@ +'use strict'; + +var utils = require('../utils'); + +/** + * Create an Error with the specified message, config, error code, request and response. + * + * @param {string} message The error message. + * @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, code, config, request, response) { + Error.call(this); + this.message = message; + this.name = 'AxiosError'; + code && (this.code = code); + config && (this.config = config); + request && (this.request = request); + response && (this.response = response); +} + +utils.inherits(AxiosError, Error, { + toJSON: function toJSON() { + return { + // Standard + message: this.message, + name: this.name, + // Microsoft + description: this.description, + number: this.number, + // Mozilla + fileName: this.fileName, + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + stack: this.stack, + // Axios + config: this.config, + 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, code, config, request, response, customProps) { + var axiosError = Object.create(prototype); + + utils.toFlatObject(error, axiosError, function filter(obj) { + return obj !== Error.prototype; + }); + + AxiosError.call(axiosError, error.message, code, config, request, response); + + axiosError.name = error.name; + + customProps && Object.assign(axiosError, customProps); + + return axiosError; +}; + +module.exports = AxiosError; diff --git a/lib/core/createError.js b/lib/core/createError.js deleted file mode 100644 index 933680f694..0000000000 --- a/lib/core/createError.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var enhanceError = require('./enhanceError'); - -/** - * 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 {Object} [request] The request. - * @param {Object} [response] The response. - * @returns {Error} The created error. - */ -module.exports = function createError(message, config, code, request, response) { - var error = new Error(message); - return enhanceError(error, config, code, request, response); -}; diff --git a/lib/core/dispatchRequest.js b/lib/core/dispatchRequest.js index 9ce3b96e6d..fa1ad95765 100644 --- a/lib/core/dispatchRequest.js +++ b/lib/core/dispatchRequest.js @@ -4,14 +4,19 @@ var utils = require('./../utils'); var transformData = require('./transformData'); var isCancel = require('../cancel/isCancel'); var defaults = require('../defaults'); +var CanceledError = require('../cancel/CanceledError'); /** - * Throws a `Cancel` if cancellation has been requested. + * Throws a `CanceledError` if cancellation has been requested. */ function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } + + if (config.signal && config.signal.aborted) { + throw new CanceledError(); + } } /** diff --git a/lib/core/enhanceError.js b/lib/core/enhanceError.js index db04ec8ea0..e69de29bb2 100644 --- a/lib/core/enhanceError.js +++ b/lib/core/enhanceError.js @@ -1,43 +0,0 @@ -'use strict'; - -/** - * Update an Error with the specified config, error code, and response. - * - * @param {Error} error The error to update. - * @param {Object} config The config. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @param {Object} [request] The request. - * @param {Object} [response] The response. - * @returns {Error} The error. - */ -module.exports = function enhanceError(error, config, code, request, response) { - error.config = config; - if (code) { - error.code = code; - } - - error.request = request; - error.response = response; - error.isAxiosError = true; - - error.toJSON = function toJSON() { - return { - // Standard - message: this.message, - name: this.name, - // Microsoft - description: this.description, - number: this.number, - // Mozilla - fileName: this.fileName, - lineNumber: this.lineNumber, - columnNumber: this.columnNumber, - stack: this.stack, - // Axios - config: this.config, - code: this.code, - status: this.response && this.response.status ? this.response.status : null - }; - }; - return error; -}; diff --git a/lib/core/mergeConfig.js b/lib/core/mergeConfig.js index 5a2c10cb01..05d1438805 100644 --- a/lib/core/mergeConfig.js +++ b/lib/core/mergeConfig.js @@ -15,17 +15,6 @@ module.exports = function mergeConfig(config1, config2) { config2 = config2 || {}; var config = {}; - var valueFromConfig2Keys = ['url', 'method', 'data']; - var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params']; - var defaultToConfig2Keys = [ - 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', - 'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', - 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress', - 'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent', - 'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding' - ]; - var directMergeKeys = ['validateStatus']; - function getMergedValue(target, source) { if (utils.isPlainObject(target) && utils.isPlainObject(source)) { return utils.merge(target, source); @@ -37,51 +26,74 @@ module.exports = function mergeConfig(config1, config2) { return source; } + // eslint-disable-next-line consistent-return function mergeDeepProperties(prop) { if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(config1[prop], config2[prop]); + return getMergedValue(config1[prop], config2[prop]); } else if (!utils.isUndefined(config1[prop])) { - config[prop] = getMergedValue(undefined, config1[prop]); + return getMergedValue(undefined, config1[prop]); } } - utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { + // eslint-disable-next-line consistent-return + function valueFromConfig2(prop) { if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(undefined, config2[prop]); + return getMergedValue(undefined, config2[prop]); } - }); - - utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties); + } - utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { + // eslint-disable-next-line consistent-return + function defaultToConfig2(prop) { if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(undefined, config2[prop]); + return getMergedValue(undefined, config2[prop]); } else if (!utils.isUndefined(config1[prop])) { - config[prop] = getMergedValue(undefined, config1[prop]); + return getMergedValue(undefined, config1[prop]); } - }); + } - utils.forEach(directMergeKeys, function merge(prop) { + // eslint-disable-next-line consistent-return + function mergeDirectKeys(prop) { if (prop in config2) { - config[prop] = getMergedValue(config1[prop], config2[prop]); + return getMergedValue(config1[prop], config2[prop]); } else if (prop in config1) { - config[prop] = getMergedValue(undefined, config1[prop]); + return getMergedValue(undefined, config1[prop]); } - }); - - var axiosKeys = valueFromConfig2Keys - .concat(mergeDeepPropertiesKeys) - .concat(defaultToConfig2Keys) - .concat(directMergeKeys); + } - var otherKeys = Object - .keys(config1) - .concat(Object.keys(config2)) - .filter(function filterAxiosKeys(key) { - return axiosKeys.indexOf(key) === -1; - }); + var mergeMap = { + 'url': valueFromConfig2, + 'method': valueFromConfig2, + 'data': valueFromConfig2, + 'baseURL': defaultToConfig2, + 'transformRequest': defaultToConfig2, + 'transformResponse': defaultToConfig2, + 'paramsSerializer': defaultToConfig2, + 'timeout': defaultToConfig2, + 'timeoutMessage': defaultToConfig2, + 'withCredentials': defaultToConfig2, + 'adapter': defaultToConfig2, + 'responseType': defaultToConfig2, + 'xsrfCookieName': defaultToConfig2, + 'xsrfHeaderName': defaultToConfig2, + 'onUploadProgress': defaultToConfig2, + 'onDownloadProgress': defaultToConfig2, + 'decompress': defaultToConfig2, + 'maxContentLength': defaultToConfig2, + 'maxBodyLength': defaultToConfig2, + 'transport': defaultToConfig2, + 'httpAgent': defaultToConfig2, + 'httpsAgent': defaultToConfig2, + 'cancelToken': defaultToConfig2, + 'socketPath': defaultToConfig2, + 'responseEncoding': defaultToConfig2, + 'validateStatus': mergeDirectKeys + }; - utils.forEach(otherKeys, mergeDeepProperties); + utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) { + var merge = mergeMap[prop] || mergeDeepProperties; + var configValue = merge(prop); + (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); + }); return config; }; diff --git a/lib/core/settle.js b/lib/core/settle.js index 886adb0c1f..bbb9afa8cb 100644 --- a/lib/core/settle.js +++ b/lib/core/settle.js @@ -1,6 +1,6 @@ 'use strict'; -var createError = require('./createError'); +var AxiosError = require('./AxiosError'); /** * Resolve or reject a Promise based on response status. @@ -14,10 +14,10 @@ module.exports = function settle(resolve, reject, response) { if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { - reject(createError( + 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 fb96bdbd13..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' @@ -26,6 +26,21 @@ function getDefaultAdapter() { return adapter; } +function stringifySafely(rawValue, parser, encoder) { + if (utils.isString(rawValue)) { + try { + (parser || JSON.parse)(rawValue); + return utils.trim(rawValue); + } catch (e) { + if (e.name !== 'SyntaxError') { + throw e; + } + } + } + + return (encoder || JSON.stringify)(rawValue); +} + var defaults = { transitional: { @@ -58,13 +73,13 @@ var defaults = { } if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) { setContentTypeIfUnset(headers, 'application/json'); - return JSON.stringify(data); + return stringifySafely(data); } return data; }], transformResponse: [function transformResponse(data) { - var transitional = this.transitional; + var transitional = this.transitional || defaults.transitional; var silentJSONParsing = transitional && transitional.silentJSONParsing; var forcedJSONParsing = transitional && transitional.forcedJSONParsing; var strictJSONParsing = !silentJSONParsing && this.responseType === 'json'; @@ -75,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; } @@ -99,12 +114,12 @@ var defaults = { validateStatus: function validateStatus(status) { return status >= 200 && status < 300; - } -}; + }, -defaults.headers = { - common: { - 'Accept': 'application/json, text/plain, */*' + headers: { + common: { + 'Accept': 'application/json, text/plain, */*' + } } }; diff --git a/lib/env/README.md b/lib/env/README.md new file mode 100644 index 0000000000..b41baff3c9 --- /dev/null +++ b/lib/env/README.md @@ -0,0 +1,3 @@ +# axios // env + +The `data.js` file is updated automatically when the package version is upgrading. Please do not edit it manually. diff --git a/lib/env/data.js b/lib/env/data.js new file mode 100644 index 0000000000..69a09266e3 --- /dev/null +++ b/lib/env/data.js @@ -0,0 +1,3 @@ +module.exports = { + "version": "0.23.0" +}; \ No newline at end of file diff --git a/lib/helpers/validator.js b/lib/helpers/validator.js index 7f1bc7dfa9..8095b90e10 100644 --- a/lib/helpers/validator.js +++ b/lib/helpers/validator.js @@ -1,6 +1,7 @@ 'use strict'; -var pkg = require('./../../package.json'); +var VERSION = require('../env/data').version; +var AxiosError = require('../core/AxiosError'); var validators = {}; @@ -12,48 +13,29 @@ var validators = {}; }); var deprecatedWarnings = {}; -var currentVerArr = pkg.version.split('.'); - -/** - * Compare package versions - * @param {string} version - * @param {string?} thanVersion - * @returns {boolean} - */ -function isOlderVersion(version, thanVersion) { - var pkgVersionArr = thanVersion ? thanVersion.split('.') : currentVerArr; - var destVer = version.split('.'); - for (var i = 0; i < 3; i++) { - if (pkgVersionArr[i] > destVer[i]) { - return true; - } else if (pkgVersionArr[i] < destVer[i]) { - return false; - } - } - return false; -} /** * Transitional option validator - * @param {function|boolean?} validator - * @param {string?} version - * @param {string} message + * @param {function|boolean?} validator - set to false if the transitional option has been removed + * @param {string?} version - deprecated version / removed since version + * @param {string?} message - some message with additional info * @returns {function} */ validators.transitional = function transitional(validator, version, message) { - var isDeprecated = version && isOlderVersion(version); - function formatMessage(opt, desc) { - return '[Axios v' + pkg.version + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : ''); + return '[Axios v' + VERSION + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : ''); } // eslint-disable-next-line func-names return function(value, opt, opts) { if (validator === false) { - throw new Error(formatMessage(opt, ' has been removed in ' + version)); + throw new AxiosError( + formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')), + AxiosError.ERR_DEPRECATED + ); } - if (isDeprecated && !deprecatedWarnings[opt]) { + if (version && !deprecatedWarnings[opt]) { deprecatedWarnings[opt] = true; // eslint-disable-next-line no-console console.warn( @@ -77,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; @@ -88,18 +70,17 @@ 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); } } } module.exports = { - isOlderVersion: isOlderVersion, assertOptions: assertOptions, validators: validators }; diff --git a/lib/utils.js b/lib/utils.js index 5d966f4448..140a37e4a7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -323,6 +323,52 @@ function stripBOM(content) { return content; } +/** + * Inherit the prototype methods from one constructor into another + * @param {function} constructor + * @param {function} superConstructor + * @param {object} [props] + * @param {object} [descriptors] + */ + +function inherits(constructor, superConstructor, props, descriptors) { + constructor.prototype = Object.create(superConstructor.prototype, descriptors); + constructor.prototype.constructor = constructor; + 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 = { isArray: isArray, isArrayBuffer: isArrayBuffer, @@ -345,5 +391,7 @@ module.exports = { merge: merge, extend: extend, trim: trim, - stripBOM: stripBOM + stripBOM: stripBOM, + inherits: inherits, + toFlatObject: toFlatObject }; diff --git a/package.json b/package.json index c61b72a99e..04a08968f6 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "axios", - "version": "0.21.3", + "version": "0.23.0", "description": "Promise based HTTP client for the browser and node.js", "main": "index.js", "types": "index.d.ts", "scripts": { - "test": "grunt test", + "test": "grunt test && dtslint", "start": "node ./sandbox/server.js", "build": "NODE_ENV=production grunt build", - "preversion": "npm test", - "version": "npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json", + "preversion": "grunt version && npm test", + "version": "npm run build && git add -A dist && git add CHANGELOG.md bower.json package.json", "postversion": "git push && git push --tags", "examples": "node ./examples/server.js", "coveralls": "cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", @@ -33,7 +33,9 @@ }, "homepage": "https://axios-http.com", "devDependencies": { + "abortcontroller-polyfill": "^1.5.0", "coveralls": "^3.0.0", + "dtslint": "^4.1.6", "es6-promise": "^4.2.4", "grunt": "^1.3.0", "grunt-banner": "^0.6.0", @@ -43,7 +45,6 @@ "grunt-eslint": "^23.0.0", "grunt-karma": "^4.0.0", "grunt-mocha-test": "^0.13.3", - "grunt-ts": "^6.0.0-beta.19", "grunt-webpack": "^4.0.2", "istanbul-instrumenter-loader": "^1.0.0", "jasmine-core": "^2.4.1", @@ -74,7 +75,7 @@ "unpkg": "dist/axios.min.js", "typings": "./index.d.ts", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.4" }, "bundlesize": [ { diff --git a/test/specs/api.spec.js b/test/specs/api.spec.js index eaeadfa2d7..73ef1d8829 100644 --- a/test/specs/api.spec.js +++ b/test/specs/api.spec.js @@ -36,7 +36,7 @@ describe('static api', function () { expect(typeof axios.create).toEqual('function'); }); - it('should have Cancel, CancelToken, and isCancel properties', function () { + it('should have CanceledError, CancelToken, and isCancel properties', function () { expect(typeof axios.Cancel).toEqual('function'); expect(typeof axios.CancelToken).toEqual('function'); expect(typeof axios.isCancel).toEqual('function'); diff --git a/test/specs/cancel.spec.js b/test/specs/cancel.spec.js index 42b2b4e27f..d4056eb48c 100644 --- a/test/specs/cancel.spec.js +++ b/test/specs/cancel.spec.js @@ -1,5 +1,8 @@ var Cancel = axios.Cancel; var CancelToken = axios.CancelToken; +var _AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill.js').AbortController; + +var AbortController = typeof AbortController === 'function' ? AbortController : _AbortController; describe('cancel', function() { beforeEach(function() { @@ -11,12 +14,12 @@ describe('cancel', function() { }); describe('when called before sending request', function() { - it('rejects Promise with a Cancel object', function (done) { + it('rejects Promise with a CanceledError object', function(done) { var source = CancelToken.source(); source.cancel('Operation has been canceled.'); axios.get('/foo', { cancelToken: source.token - }).catch(function (thrown) { + }).catch(function(thrown) { expect(thrown).toEqual(jasmine.any(Cancel)); expect(thrown.message).toBe('Operation has been canceled.'); done(); @@ -25,17 +28,17 @@ describe('cancel', function() { }); describe('when called after request has been sent', function() { - it('rejects Promise with a Cancel object', function (done) { + it('rejects Promise with a CanceledError object', function(done) { var source = CancelToken.source(); axios.get('/foo/bar', { cancelToken: source.token - }).catch(function (thrown) { + }).catch(function(thrown) { expect(thrown).toEqual(jasmine.any(Cancel)); expect(thrown.message).toBe('Operation has been canceled.'); done(); }); - getAjaxRequest().then(function (request) { + getAjaxRequest().then(function(request) { // call cancel() when the request has been sent, but a response has not been received source.cancel('Operation has been canceled.'); request.respondWith({ @@ -45,7 +48,7 @@ describe('cancel', function() { }); }); - it('calls abort on request object', function (done) { + it('calls abort on request object', function(done) { var source = CancelToken.source(); var request; axios.get('/foo/bar', { @@ -56,7 +59,7 @@ describe('cancel', function() { done(); }); - getAjaxRequest().then(function (req) { + getAjaxRequest().then(function(req) { // call cancel() when the request has been sent, but a response has not been received source.cancel(); request = req; @@ -66,19 +69,19 @@ describe('cancel', function() { describe('when called after response has been received', function() { // https://github.com/axios/axios/issues/482 - it('does not cause unhandled rejection', function (done) { + it('does not cause unhandled rejection', function(done) { var source = CancelToken.source(); axios.get('/foo', { cancelToken: source.token - }).then(function () { - window.addEventListener('unhandledrejection', function () { + }).then(function() { + window.addEventListener('unhandledrejection', function() { done.fail('Unhandled rejection.'); }); source.cancel(); setTimeout(done, 100); }); - getAjaxRequest().then(function (request) { + getAjaxRequest().then(function(request) { request.respondWith({ status: 200, responseText: 'OK' @@ -86,4 +89,30 @@ describe('cancel', function() { }); }); }); + + it('it should support cancellation using AbortController signal', function(done) { + var controller = new AbortController(); + + axios.get('/foo/bar', { + signal: controller.signal + }).then(function() { + done.fail('Has not been canceled'); + }, + function(thrown) { + expect(thrown).toEqual(jasmine.any(Cancel)); + done(); + } + ); + + getAjaxRequest().then(function (request) { + // call cancel() when the request has been sent, but a response has not been received + controller.abort(); + setTimeout(function(){ + request.respondWith({ + status: 200, + responseText: 'OK' + }); + }, 0); + }); + }); }); diff --git a/test/specs/cancel/Cancel.spec.js b/test/specs/cancel/Cancel.spec.js deleted file mode 100644 index 0e0de805bf..0000000000 --- a/test/specs/cancel/Cancel.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -var Cancel = require('../../../lib/cancel/Cancel'); - -describe('Cancel', function() { - describe('toString', function() { - it('returns correct result when message is not specified', function() { - var cancel = new Cancel(); - expect(cancel.toString()).toBe('Cancel'); - }); - - it('returns correct result when message is specified', function() { - var cancel = new Cancel('Operation has been canceled.'); - expect(cancel.toString()).toBe('Cancel: Operation has been canceled.'); - }); - }); -}); diff --git a/test/specs/cancel/CancelToken.spec.js b/test/specs/cancel/CancelToken.spec.js index dd72327174..2fa067321e 100644 --- a/test/specs/cancel/CancelToken.spec.js +++ b/test/specs/cancel/CancelToken.spec.js @@ -1,5 +1,5 @@ var CancelToken = require('../../../lib/cancel/CancelToken'); -var Cancel = require('../../../lib/cancel/Cancel'); +var CanceledError = require('../../../lib/cancel/CanceledError'); describe('CancelToken', function() { describe('constructor', function() { @@ -17,13 +17,13 @@ describe('CancelToken', function() { }); describe('reason', function() { - it('returns a Cancel if cancellation has been requested', function() { + it('returns a CanceledError if cancellation has been requested', function() { var cancel; var token = new CancelToken(function(c) { cancel = c; }); cancel('Operation has been canceled.'); - expect(token.reason).toEqual(jasmine.any(Cancel)); + expect(token.reason).toEqual(jasmine.any(CanceledError)); expect(token.reason.message).toBe('Operation has been canceled.'); }); @@ -40,7 +40,7 @@ describe('CancelToken', function() { cancel = c; }); token.promise.then(function onFulfilled(value) { - expect(value).toEqual(jasmine.any(Cancel)); + expect(value).toEqual(jasmine.any(CanceledError)); expect(value.message).toBe('Operation has been canceled.'); done(); }); @@ -50,7 +50,7 @@ describe('CancelToken', function() { describe('throwIfRequested', function() { it('throws if cancellation has been requested', function() { - // Note: we cannot use expect.toThrowError here as Cancel does not inherit from Error + // Note: we cannot use expect.toThrowError here as CanceledError does not inherit from Error var cancel; var token = new CancelToken(function(c) { cancel = c; @@ -60,8 +60,8 @@ describe('CancelToken', function() { token.throwIfRequested(); fail('Expected throwIfRequested to throw.'); } catch (thrown) { - if (!(thrown instanceof Cancel)) { - fail('Expected throwIfRequested to throw a Cancel, but it threw ' + thrown + '.'); + if (!(thrown instanceof CanceledError)) { + fail('Expected throwIfRequested to throw a CanceledError, but it threw ' + thrown + '.'); } expect(thrown.message).toBe('Operation has been canceled.'); } @@ -80,7 +80,7 @@ describe('CancelToken', function() { expect(source.cancel).toEqual(jasmine.any(Function)); expect(source.token.reason).toBeUndefined(); source.cancel('Operation has been canceled.'); - expect(source.token.reason).toEqual(jasmine.any(Cancel)); + expect(source.token.reason).toEqual(jasmine.any(CanceledError)); expect(source.token.reason.message).toBe('Operation has been canceled.'); }); }); diff --git a/test/specs/cancel/CanceledError.spec.js b/test/specs/cancel/CanceledError.spec.js new file mode 100644 index 0000000000..de996d581a --- /dev/null +++ b/test/specs/cancel/CanceledError.spec.js @@ -0,0 +1,15 @@ +var CanceledError = require('../../../lib/cancel/CanceledError'); + +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: canceled'); + }); + + it('returns correct result when message is specified', function() { + var cancel = new CanceledError('Operation has been canceled.'); + expect(cancel.toString()).toBe('CanceledError: Operation has been canceled.'); + }); + }); +}); diff --git a/test/specs/cancel/isCancel.spec.js b/test/specs/cancel/isCancel.spec.js index e6be40dac9..aac264bdc4 100644 --- a/test/specs/cancel/isCancel.spec.js +++ b/test/specs/cancel/isCancel.spec.js @@ -1,12 +1,12 @@ var isCancel = require('../../../lib/cancel/isCancel'); -var Cancel = require('../../../lib/cancel/Cancel'); +var CanceledError = require('../../../lib/cancel/CanceledError'); describe('isCancel', function() { - it('returns true if value is a Cancel', function() { - expect(isCancel(new Cancel())).toBe(true); + it('returns true if value is a CanceledError', function() { + expect(isCancel(new CanceledError())).toBe(true); }); - it('returns false if value is not a Cancel', function() { + it('returns false if value is not a CanceledError', function() { expect(isCancel({ foo: 'bar' })).toBe(false); }); }); diff --git a/test/specs/core/AxiosError.spec.js b/test/specs/core/AxiosError.spec.js new file mode 100644 index 0000000000..49a9a6fb0d --- /dev/null +++ b/test/specs/core/AxiosError.spec.js @@ -0,0 +1,50 @@ +var AxiosError = require('../../../lib/core/AxiosError'); + +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!', 'ESOMETHING', { foo: 'bar' }, request, response); + expect(error instanceof Error).toBe(true); + expect(error.message).toBe('Boom!'); + expect(error.config).toEqual({ foo: 'bar' }); + expect(error.code).toBe('ESOMETHING'); + expect(error.request).toBe(request); + expect(error.response).toBe(response); + expect(error.isAxiosError).toBe(true); + }); + it('should create an Error that can be serialized to JSON', function() { + // Attempting to serialize request and response results in + // TypeError: Converting circular structure to JSON + var request = { path: '/foo' }; + var response = { status: 200, data: { foo: 'bar' } }; + 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' }); + expect(json.code).toBe('ESOMETHING'); + expect(json.status).toBe(200); + expect(json.request).toBe(undefined); + expect(json.response).toBe(undefined); + }); + + describe('core::createError.from', function() { + it('should add config, config, request and response to error', function() { + var error = new Error('Boom!'); + var request = { path: '/foo' }; + var response = { status: 200, data: { foo: 'bar' } }; + + 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); + expect(axiosError.response).toBe(response); + expect(axiosError.isAxiosError).toBe(true); + }); + + it('should return error', function() { + var error = new Error('Boom!'); + expect(AxiosError.from(error, 'ESOMETHING', { foo: 'bar' }) instanceof AxiosError).toBeTruthy(); + }); + }); +}); diff --git a/test/specs/core/createError.spec.js b/test/specs/core/createError.spec.js deleted file mode 100644 index 7b1f6c699c..0000000000 --- a/test/specs/core/createError.spec.js +++ /dev/null @@ -1,30 +0,0 @@ -var createError = require('../../../lib/core/createError'); - -describe('core::createError', 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 = createError('Boom!', { foo: 'bar' }, 'ESOMETHING', request, response); - expect(error instanceof Error).toBe(true); - expect(error.message).toBe('Boom!'); - expect(error.config).toEqual({ foo: 'bar' }); - expect(error.code).toBe('ESOMETHING'); - expect(error.request).toBe(request); - expect(error.response).toBe(response); - expect(error.isAxiosError).toBe(true); - }); - it('should create an Error that can be serialized to JSON', function() { - // Attempting to serialize request and response results in - // TypeError: Converting circular structure to JSON - var request = { path: '/foo' }; - var response = { status: 200, data: { foo: 'bar' } }; - var error = createError('Boom!', { foo: 'bar' }, 'ESOMETHING', request, response); - var json = error.toJSON(); - expect(json.message).toBe('Boom!'); - expect(json.config).toEqual({ foo: 'bar' }); - expect(json.code).toBe('ESOMETHING'); - expect(json.status).toBe(200); - expect(json.request).toBe(undefined); - expect(json.response).toBe(undefined); - }); -}); diff --git a/test/specs/core/enhanceError.spec.js b/test/specs/core/enhanceError.spec.js index 767ebfdde7..e69de29bb2 100644 --- a/test/specs/core/enhanceError.spec.js +++ b/test/specs/core/enhanceError.spec.js @@ -1,31 +0,0 @@ -var enhanceError = require('../../../lib/core/enhanceError'); - -describe('core::enhanceError', function() { - it('should add config, code, request, response, and toJSON function to error', function() { - var error = new Error('Boom!'); - var request = { path: '/foo' }; - var response = { status: 200, data: { foo: 'bar' } }; - - enhanceError(error, { foo: 'bar' }, 'ESOMETHING', request, response); - expect(error.config).toEqual({ foo: 'bar' }); - expect(error.code).toBe('ESOMETHING'); - expect(error.request).toBe(request); - expect(error.response).toBe(response); - expect(typeof error.toJSON).toBe('function'); - expect(error.isAxiosError).toBe(true); - }); - - it('should serialize to JSON with a status of null when there is no response', function() { - var error = new Error('Boom!'); - var request = { path: '/foo' }; - var response = undefined; - - var errorAsJson = enhanceError(error, { foo: 'bar' }, 'ESOMETHING', request, response).toJSON(); - expect(errorAsJson.status).toEqual(null); - }); - - it('should return error', function() { - var error = new Error('Boom!'); - expect(enhanceError(error, { foo: 'bar' }, 'ESOMETHING')).toBe(error); - }); -}); diff --git a/test/specs/defaults.spec.js b/test/specs/defaults.spec.js index d71cd66a4e..ff0f15febe 100644 --- a/test/specs/defaults.spec.js +++ b/test/specs/defaults.spec.js @@ -20,6 +20,19 @@ describe('defaults', function () { expect(defaults.transformRequest[0]({foo: 'bar'})).toEqual('{"foo":"bar"}'); }); + it("should also transform request json when 'Content-Type' is 'application/json'", function () { + var headers = { + 'Content-Type': 'application/json', + }; + expect(defaults.transformRequest[0](JSON.stringify({ foo: 'bar' }), headers)).toEqual('{"foo":"bar"}'); + expect(defaults.transformRequest[0]([42, 43], headers)).toEqual('[42,43]'); + expect(defaults.transformRequest[0]('foo', headers)).toEqual('"foo"'); + expect(defaults.transformRequest[0](42, headers)).toEqual('42'); + expect(defaults.transformRequest[0](true, headers)).toEqual('true'); + expect(defaults.transformRequest[0](false, headers)).toEqual('false'); + expect(defaults.transformRequest[0](null, headers)).toEqual('null'); + }); + it('should do nothing to request string', function () { expect(defaults.transformRequest[0]('foo=bar')).toEqual('foo=bar'); }); diff --git a/test/specs/helpers/isAxiosError.spec.js b/test/specs/helpers/isAxiosError.spec.js index 7aeef85f53..1f8b3a15db 100644 --- a/test/specs/helpers/isAxiosError.spec.js +++ b/test/specs/helpers/isAxiosError.spec.js @@ -1,19 +1,18 @@ -var createError = require('../../../lib/core/createError'); -var enhanceError = require('../../../lib/core/enhanceError'); +var AxiosError = require('../../../lib/core/AxiosError'); 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(createError('Boom!', { foo: 'bar' }))) +describe('helpers::isAxiosError', function() { + it('should return true if the error is created by core::createError', function() { + 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(enhanceError(new Error('Boom!'), { foo: 'bar' }))) + it('should return true if the error is enhanced by core::enhanceError', function() { + expect(isAxiosError(AxiosError.from(new Error('Boom!'), null, { foo: 'bar' }))) .toBe(true); }); - it('should return false if the error is a normal Error instance', function () { + it('should return false if the error is a normal Error instance', function() { expect(isAxiosError(new Error('Boom!'))) .toBe(false); }); diff --git a/test/specs/helpers/validator.spec.js b/test/specs/helpers/validator.spec.js index 9a125d4139..6f2aa0c507 100644 --- a/test/specs/helpers/validator.spec.js +++ b/test/specs/helpers/validator.spec.js @@ -2,23 +2,8 @@ var validator = require('../../../lib/helpers/validator'); -describe('validator::isOlderVersion', function () { - it('should return true if dest version is older than the package version', function () { - expect(validator.isOlderVersion('0.0.1', '1.0.0')).toEqual(true); - expect(validator.isOlderVersion('0.0.1', '0.1.0')).toEqual(true); - expect(validator.isOlderVersion('0.0.1', '0.0.1')).toEqual(false); - - - expect(validator.isOlderVersion('100.0.0', '1.0.0')).toEqual(false); - expect(validator.isOlderVersion('100.0.0', '0.1.0')).toEqual(false); - expect(validator.isOlderVersion('100.0.0', '0.0.1')).toEqual(false); - - expect(validator.isOlderVersion('0.10000.0', '1000.0.1')).toEqual(true); - }); -}); - -describe('validator::assertOptions', function () { - it('should throw only if unknown an option was passed', function () { +describe('validator::assertOptions', function() { + it('should throw only if unknown an option was passed', function() { expect(function() { validator.assertOptions({ x: true @@ -37,7 +22,7 @@ describe('validator::assertOptions', function () { }).not.toThrow(new Error('Unknown option x')); }); - it('should throw TypeError only if option type doesn\'t match', function () { + it('should throw TypeError only if option type doesn\'t match', function() { expect(function() { validator.assertOptions({ x: 123 diff --git a/test/specs/instance.spec.js b/test/specs/instance.spec.js index bd9e9fbd62..e579141c29 100644 --- a/test/specs/instance.spec.js +++ b/test/specs/instance.spec.js @@ -13,13 +13,16 @@ describe('instance', function () { for (var prop in axios) { if ([ 'Axios', + 'AxiosError', 'create', 'Cancel', + 'CanceledError', 'CancelToken', 'isCancel', 'all', 'spread', 'isAxiosError', + 'VERSION', 'default'].indexOf(prop) > -1) { continue; } 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/typescript/axios.ts b/test/typescript/axios.ts index 999b3630bc..20cf05836f 100644 --- a/test/typescript/axios.ts +++ b/test/typescript/axios.ts @@ -8,7 +8,7 @@ import axios, { CancelToken, CancelTokenSource, Canceler -} from '../../'; +} from 'axios'; const config: AxiosRequestConfig = { url: '/user', @@ -170,8 +170,8 @@ axios.patch('/user', { name: 'foo', id: 1 }) // (Typed methods) with custom response type const handleStringResponse = (response: string) => { - console.log(response) -} + console.log(response); +}; axios.get('/user?id=12345') .then(handleStringResponse) @@ -342,11 +342,11 @@ axios.get('/user') axios.get('/user') .catch((error: any) => 'foo') - .then((value: string) => {}); + .then((value) => {}); axios.get('/user') .catch((error: any) => Promise.resolve('foo')) - .then((value: string) => {}); + .then((value) => {}); // Cancellation diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 6d2c040e06..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 () { @@ -18,7 +19,7 @@ describe('supports http with nodejs', function () { server = null; } if (proxy) { - proxy.close() + proxy.close(); proxy = null; } if (process.env.http_proxy) { @@ -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); @@ -326,7 +327,7 @@ describe('supports http with nodejs', function () { res.end(req.headers.authorization); }).listen(4444, function () { var auth = { username: 'foo', password: 'bar' }; - var headers = { Authorization: 'Bearer 1234' }; + var headers = { AuThOrIzAtIoN: 'Bearer 1234' }; // wonky casing to ensure caseless comparison axios.get('http://localhost:4444/', { auth: auth, headers: headers }).then(function (res) { var base64 = Buffer.from('foo:bar', 'utf8').toString('base64'); assert.equal(res.data, 'Basic ' + base64); @@ -335,6 +336,41 @@ describe('supports http with nodejs', function () { }); }); + it('should provides a default User-Agent header', function (done) { + server = http.createServer(function (req, res) { + res.end(req.headers['user-agent']); + }).listen(4444, function () { + axios.get('http://localhost:4444/').then(function (res) { + assert.ok(/^axios\/[\d.]+$/.test(res.data), `User-Agent header does not match: ${res.data}`); + done(); + }); + }); + }); + + it('should allow the User-Agent header to be overridden', function (done) { + server = http.createServer(function (req, res) { + res.end(req.headers['user-agent']); + }).listen(4444, function () { + var headers = { 'UsEr-AgEnT': 'foo bar' }; // wonky casing to ensure caseless comparison + axios.get('http://localhost:4444/', { headers }).then(function (res) { + assert.equal(res.data, 'foo bar'); + done(); + }); + }); + }); + + it('should allow the Content-Length header to be overridden', function (done) { + server = http.createServer(function (req, res) { + assert.strictEqual(req.headers['content-length'], '42'); + res.end(); + }).listen(4444, function () { + var headers = { 'CoNtEnT-lEnGtH': '42' }; // wonky casing to ensure caseless comparison + axios.post('http://localhost:4444/', 'foo', { headers }).then(function () { + done(); + }); + }); + }); + it('should support max content length', function (done) { var str = Array(100000).join('ж'); @@ -918,7 +954,7 @@ describe('supports http with nodejs', function () { axios.get('http://localhost:4444/', { cancelToken: source.token }).catch(function (thrown) { - assert.ok(thrown instanceof axios.Cancel, 'Promise must be rejected with a Cancel obejct'); + assert.ok(thrown instanceof axios.Cancel, 'Promise must be rejected with a CanceledError object'); assert.equal(thrown.message, 'Operation has been canceled.'); done(); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..6665188255 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "es2015", + "lib": ["dom", "es2015"], + "types": [], + "moduleResolution": "node", + "strict": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "axios": ["."] + } + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000000..3ec44a760a --- /dev/null +++ b/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": "dtslint/dtslint.json", + "rules": { + "no-unnecessary-generics": false + } +} diff --git a/webpack.config.js b/webpack.config.js index 9dba880a72..69e0323fdb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,7 +11,8 @@ function generateConfig(name) { filename: name + '.js', sourceMapFilename: name + '.map', library: 'axios', - libraryTarget: 'umd' + libraryTarget: 'umd', + globalObject: 'this' }, node: { process: false