From d3b4912b35fff3e7b629acd5b4de3aaeaa743d25 Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 28 May 2022 11:55:51 +0200 Subject: [PATCH] Update v1.x branch with proposed release changes (#4750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixing proxy beforeRedirect regression (#4708) * Adding Canceler parameters config and request (#4711) Co-authored-by: Jay * Fixed `toFormData` regression bug (unreleased) with Array-like objects serialization; (#4714) Added `toURLEncodedForm` helper; Added automatic payload serialization to `application/x-www-form-urlencoded` to have parity with `multipart/form-data`; Added test of handling `application/x-www-form-urlencoded` body by express.js; Updated README.md; Added missed param in JSDoc; Fixed hrefs in README.md; Co-authored-by: Jay * Allow webpack users to overwrite buildins (#4715) Co-authored-by: Jay * Fixed `AxiosError` status code type; (#4717) Co-authored-by: Jay * Fixed `AxiosError` stack capturing; (#4718) Co-authored-by: Jay * allow type definition for axios instance methods (#4224) Co-authored-by: Jay * add `string[]` to `AxiosRequestHeaders` type (#4322) Co-authored-by: Jay * Fixing AxiosRequestHeaders typings (#4334) Co-authored-by: Shakirov Kirill Co-authored-by: Jay * Added the ability for the `url-encoded-form` serializer to respect the `formSerializer` config; (#4721) Added test for `formSerializer` config in context of `url-encoded-form` serializer; * Updated eslint config; (#4722) Co-authored-by: Jay * fix: add isCancel type assert (#4293) Co-authored-by: Jay * Added data URL support for node.js; (#4725) * Added data URL support for node.js; Added missed data URL protocol for the browser environment; Optimized JSON parsing in the default response transformer; Refactored project structure; Added `cause` prop for AxiosError instance that refers to the original error if it was wrapped with `AxiosError.from` method; Added fromDataURI helper; Added test for handling data:url as an `arraybuffer|text|stream`; * Added throwing of 405 HTTP error if the method is not GET; * Fix/4263/maxbodylength defaults (#4731) * test(http): add test case for default body length in follow-redirects * fix(http): provide proper default body length to follow-redirects Co-authored-by: Jay * Adding types for progress event callbacks (#4675) Co-authored-by: Jay * Fixed bug #4727 : toFormData Blob issue on node>v17; (#4728) * Fixed bug #4727; Added node 18.x to the CI; Added hotfix for `ERR_OSSL_EVP_UNSUPPORTED` issue with karma running on node >=17.x; Added `cross-env` to allow running build and test scripts on Windows platforms; * Added conditional setting of `--openssl-legacy-provider` option for node versions >=17.x; * Refactored ssl-hotfix & test script; * Fixed and refactored default max body length test due to ECONNRESET failure; * Added test for converting the data uri to a Blob; Fixed bug with parsing mime type for Blob; Co-authored-by: Jay * URL params serializer; (#4734) * Refactored BuildURL helper to use URLSearchParams serializer; * Updated typings; Added TS test; * Added `axios.formToJSON` method; (#4735) * Draft * Added `formDataToJSON` helper; Added `axios.formToJSON` method; Added client tests; Co-authored-by: Jay * Bump grunt from 1.5.2 to 1.5.3 (#4743) Bumps [grunt](https://github.com/gruntjs/grunt) from 1.5.2 to 1.5.3. - [Release notes](https://github.com/gruntjs/grunt/releases) - [Changelog](https://github.com/gruntjs/grunt/blob/main/CHANGELOG) - [Commits](https://github.com/gruntjs/grunt/compare/v1.5.2...v1.5.3) --- updated-dependencies: - dependency-name: grunt dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Updated README.md; (#4742) Updated index.d.ts; Co-authored-by: Jay * chore: removed Travis CI config file as we have moved to GitHub actions * chore: updated actions to run on new version based branches * Fix/4737/timeout error message for http (#4738) * Fixing timeoutErrorMessage in http calls When timeoutErrorMessage was set this did not change anything in the error message, with this change the error message will be the configured message * Testing timeoutErrorMessage in http calls When timeoutErrorMessage was set this did not change anything in the error message, with this change the error message will be the configured message Co-authored-by: Jay * Fixing content-type header repeated (#4745) Co-authored-by: Jay Co-authored-by: Maxime Bargiel Co-authored-by: 毛呆 Co-authored-by: Dmitriy Mozgovoy Co-authored-by: Tom Ceuppens Co-authored-by: Jelle Schutter Co-authored-by: Rraji Abdelbari <57002508+estarossa0@users.noreply.github.com> Co-authored-by: Kirill Shakirov Co-authored-by: Shakirov Kirill Co-authored-by: chenjigeng <178854407@qq.com> Co-authored-by: Dimitris Halatsis Co-authored-by: Johann Cooper Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Augusto Co-authored-by: JoΓ£o Gabriel Quaresma --- .eslintrc.js | 12 +- .github/workflows/ci.yml | 6 +- .travis.yml | 16 -- README.md | 211 ++++++++++++++--- bin/ssl_hotfix.js | 22 ++ index.d.ts | 68 ++++-- lib/adapters/http.js | 134 ++++++++--- lib/adapters/xhr.js | 3 +- lib/axios.js | 6 +- lib/cancel/CancelToken.js | 4 +- lib/core/Axios.js | 3 +- lib/core/AxiosError.js | 10 +- lib/core/dispatchRequest.js | 4 + lib/defaults/index.js | 62 +++-- lib/{defaults/env => env/classes}/FormData.js | 0 lib/helpers/AxiosURLSearchParams.js | 42 ++++ lib/helpers/buildURL.js | 51 ++-- lib/helpers/formDataToJSON.js | 71 ++++++ lib/helpers/fromDataURI.js | 51 ++++ lib/helpers/toFormData.js | 33 ++- lib/helpers/toURLEncodedForm.js | 18 ++ lib/platform/browser/classes/FormData.js | 3 + .../browser/classes/URLSearchParams.js | 5 + lib/platform/browser/index.js | 11 + lib/platform/index.js | 3 + lib/platform/node/classes/FormData.js | 3 + lib/platform/node/classes/URLSearchParams.js | 5 + lib/platform/node/index.js | 11 + lib/utils.js | 53 ++++- package-lock.json | 50 +++- package.json | 8 +- test/specs/helpers/buildURL.spec.js | 11 +- test/specs/helpers/formDataToJSON.spec.js | 50 ++++ test/specs/instance.spec.js | 3 +- test/specs/transform.spec.js | 20 ++ test/typescript/axios.ts | 5 +- test/unit/adapters/http.js | 222 +++++++++++++++++- test/unit/helpers/fromDataURI.js | 12 + 38 files changed, 1080 insertions(+), 222 deletions(-) delete mode 100644 .travis.yml create mode 100644 bin/ssl_hotfix.js rename lib/{defaults/env => env/classes}/FormData.js (100%) create mode 100644 lib/helpers/AxiosURLSearchParams.js create mode 100644 lib/helpers/formDataToJSON.js create mode 100644 lib/helpers/fromDataURI.js create mode 100644 lib/helpers/toURLEncodedForm.js create mode 100644 lib/platform/browser/classes/FormData.js create mode 100644 lib/platform/browser/classes/URLSearchParams.js create mode 100644 lib/platform/browser/index.js create mode 100644 lib/platform/index.js create mode 100644 lib/platform/node/classes/FormData.js create mode 100644 lib/platform/node/classes/URLSearchParams.js create mode 100644 lib/platform/node/index.js create mode 100644 test/specs/helpers/formDataToJSON.spec.js create mode 100644 test/unit/helpers/fromDataURI.js diff --git a/.eslintrc.js b/.eslintrc.js index 0802a14f1b..efbcf49f2b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,17 +54,17 @@ module.exports = { /** * Best practices */ - 'consistent-return': 2, // http://eslint.org/docs/rules/consistent-return + 'consistent-return': 0, // 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 + 'eqeqeq': [2, "smart"], // 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-eq-null': 0, // 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 @@ -80,7 +80,7 @@ module.exports = { '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-param-reassign': 0, // 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 @@ -114,9 +114,7 @@ module.exports = { }], 'comma-style': [2, 'last'], // http://eslint.org/docs/rules/comma-style 'eol-last': 2, // http://eslint.org/docs/rules/eol-last - 'func-names': [ - 1, 'as-needed' - ], // http://eslint.org/docs/rules/func-names + 'func-names': 0, // http://eslint.org/docs/rules/func-names 'key-spacing': [2, { // http://eslint.org/docs/rules/key-spacing 'beforeColon': false, 'afterColon': true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1475ae36a..f81e012c61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: ci on: push: - branches: [master, 'release/*', dev] + branches: [master, 'v1.x/*', 'v2.x/*'] pull_request: - branches: [master, 'release/*', dev] + branches: [master, 'v1.x/*', 'v2.x/*'] jobs: build: @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [12.x, 14.x, 16.x, 18.x] steps: - uses: actions/checkout@v2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 51040e49f1..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - - xvfb -language: node_js -node_js: - - 10 - - 12 - - 14 -email: - on_failure: change - on_success: never -after_success: -- npm run coveralls -env: - global: - - secure: LlXIBEaBLgJznkHWfTV6aftkGoBjH2vik4ZQhKq4k5pvoPLD+n5n28+0bjwlzDIHUdHb+n2YXtyM2PGvGzuqwltV+UY1gu0uG2RNR+5CBsp0pOr0FfGXK6YMXn0BYER6tGYIhaG7ElHBEO0SLcQeQV/xN/m3leyawbKEMBUGizU= - - secure: XbXYzVddHJSVdbJRd/YtsdNu6Wlgx3pXvpuBpg9qBc3TytAF4LzhJNI8u1p4D1Gn8wANlxv1GNgEgkecxbzlTPST+mUrd6KlPLa1+Cmffgajr4oQjsh9ILKMe5Haqx8FOVrPK/leB1mi52liNLlkuo3/BK2r/tC2kMji+2zbses= diff --git a/README.md b/README.md index 94511703c1..45b80b2d62 100755 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ [![gitter chat](https://img.shields.io/gitter/room/mzabriskie/axios.svg?style=flat-square)](https://gitter.im/mzabriskie/axios) [![code helpers](https://www.codetriage.com/axios/axios/badges/users.svg)](https://www.codetriage.com/axios/axios) [![Known Vulnerabilities](https://snyk.io/test/npm/axios/badge.svg)](https://snyk.io/test/npm/axios) +![npm bundle size](https://img.shields.io/bundlephobia/minzip/axios) Promise based HTTP client for the browser and node.js @@ -39,12 +40,14 @@ Promise based HTTP client for the browser and node.js - [AbortController](#abortcontroller) - [CancelToken πŸ‘Ž](#canceltoken-deprecated) - [Using application/x-www-form-urlencoded format](#using-applicationx-www-form-urlencoded-format) - - [Browser](#browser) - - [Node.js](#nodejs) - - [Query string](#query-string) - - [Form data](#form-data) - - [Automatic serialization](#-automatic-serialization) - - [Manual FormData passing](#manual-formdata-passing) + - [URLSearchParams](#urlsearchparams) + - [Query string](#query-string-older-browsers) + - [πŸ†• Automatic serialization](#-automatic-serialization-to-urlsearchparams) + - [Using multipart/form-data format](#using-multipartform-data-format) + - [FormData](#formdata) + - [πŸ†• Automatic serialization](#-automatic-serialization-to-formdata) + - [Files Posting](#files-posting) + - [HTML Form Posting](#html-form-posting-browser) - [Semver](#semver) - [Promises](#promises) - [TypeScript](#typescript) @@ -61,6 +64,7 @@ Promise based HTTP client for the browser and node.js - Transform request and response data - Cancel requests - Automatic transforms for JSON data +- πŸ†• Automatic data object serialization to `multipart/form-data` and `x-www-form-urlencoded` body encodings - Client side support for protecting against [XSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) ## Browser Support @@ -336,10 +340,9 @@ These are the available config options for making requests. Only the `url` is re ID: 12345 }, - // `paramsSerializer` is an optional function in charge of serializing `params` - // (e.g. https://www.npmjs.com/package/qs, https://api.jquery.com/jquery.param/) - paramsSerializer: function (params) { - return Qs.stringify(params, {arrayFormat: 'brackets'}) + // `paramsSerializer` is an optional config in charge of serializing `params` + paramsSerializer: { + indexes: null // array indexes format (null - no brackets, false - empty brackets, true - brackets with indexes) }, // `data` is the data to be sent as the request body @@ -509,6 +512,13 @@ These are the available config options for making requests. Only the `url` is re env: { // The FormData class to be used to automatically serialize the payload into a FormData object FormData: window?.FormData || global?.FormData + }, + + formSerializer: { + visitor: (value, key, path, helpers)=> {}; // custom visitor funaction to serrialize form values + dots: boolean; // use dots instead of brackets format + metaTokens: boolean; // keep special endings like {} in parameter key + indexes: boolean; // array indexes format null - no brackets, false - empty brackets, true - brackets with indexes } } ``` @@ -814,7 +824,9 @@ cancel(); > During the transition period, you can use both cancellation APIs, even for the same request: -## Using application/x-www-form-urlencoded format +## Using `application/x-www-form-urlencoded` format + +### URLSearchParams By default, axios serializes JavaScript objects to `JSON`. To send data in the [`application/x-www-form-urlencoded` format](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) instead, you can use the [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) API, which is [supported](http://www.caniuse.com/#feat=urlsearchparams) in the vast majority of browsers, [and Node](https://nodejs.org/api/url.html#url_class_urlsearchparams) starting with v10 (released in 2018). @@ -824,7 +836,7 @@ params.append('extraparam', 'value'); axios.post('/foo', params); ``` -### Older browsers +### Query string (Older browsers) For compatibility with very old browsers, there is a [polyfill](https://github.com/WebReflection/url-search-params) available (make sure to polyfill the global environment). @@ -863,9 +875,83 @@ You can also use the [`qs`](https://github.com/ljharb/qs) library. > NOTE: > The `qs` library is preferable if you need to stringify nested objects, as the `querystring` method has [known issues](https://github.com/nodejs/node-v0.x-archive/issues/1665) with that use case. -#### Form data +### πŸ†• Automatic serialization to URLSearchParams + +Axios will automatically serialize the data object to urlencoded format if the content-type header is set to "application/x-www-form-urlencoded". + +``` +const data = { + x: 1, + arr: [1, 2, 3], + arr2: [1, [2], 3], + users: [{name: 'Peter', surname: 'Griffin'}, {name: 'Thomas', surname: 'Anderson'}], +}; + +await axios.postForm('https://postman-echo.com/post', data, + {headers: {'content-type': 'application/x-www-form-urlencoded'}} +); +``` + +The server will handle it as + +```js + { + x: '1', + 'arr[]': [ '1', '2', '3' ], + 'arr2[0]': '1', + 'arr2[1][0]': '2', + 'arr2[2]': '3', + 'arr3[]': [ '1', '2', '3' ], + 'users[0][name]': 'Peter', + 'users[0][surname]': 'griffin', + 'users[1][name]': 'Thomas', + 'users[1][surname]': 'Anderson' + } +```` + +If your backend body-parser (like `body-parser` of `express.js`) supports nested objects decoding, you will get the same object on the server-side automatically + +```js + var app = express(); + + app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies + + app.post('/', function (req, res, next) { + // echo body as JSON + res.send(JSON.stringify(req.body)); + }); + + server = app.listen(3000); +``` + +## Using `multipart/form-data` format + +### FormData + +To send the data as a `multipart/formdata` you need to pass a formData instance as a payload. +Setting the `Content-Type` header is not required as Axios guesses it based on the payload type. + +```js +const formData = new FormData(); +formData.append('foo', 'bar'); -##### πŸ†• Automatic serialization +axios.post('https://httpbin.org/post', formData); +``` + +In node.js, you can use the [`form-data`](https://github.com/form-data/form-data) library as follows: + +```js +const FormData = require('form-data'); + +const form = new FormData(); +form.append('my_field', 'my value'); +form.append('my_buffer', new Buffer(10)); +form.append('my_file', fs.createReadStream('/foo/bar.jpg')); + +axios.post('https://example.com', form) +``` + +### πŸ†• Automatic serialization to FormData Starting from `v0.27.0`, Axios supports automatic object serialization to a FormData object if the request `Content-Type` header is set to `multipart/form-data`. @@ -904,7 +990,7 @@ Axios FormData serializer supports some special endings to perform the following - `[]` - unwrap the array-like object as separate fields with the same key > NOTE: -> unwrap/expand operation will be used by default on array-like objects +> unwrap/expand operation will be used by default on arrays and FileList objects FormData serializer supports additional options via `config.formSerializer: object` property to handle rare cases: @@ -952,45 +1038,96 @@ formData.append('users[1][surname]', 'Anderson'); formData.append('obj2{}', '[{"x":1}]'); ``` +Axios supports the following shortcut methods: `postForm`, `putForm`, `patchForm` +which are just the corresponding http methods with the `Content-Type` header preset to `multipart/form-data`. + +## Files Posting + +You can easily sumbit a single file + ```js -const axios= require('axios'); +await axios.postForm('https://httpbin.org/post', { + 'myVar' : 'foo', + 'file': document.querySelector('#fileInput').files[0] +}); +``` -axios.post('https://httpbin.org/post', { - 'myObj{}': {x: 1, s: "foo"}, +or multiple files as `multipart/form-data`. + +```js +await axios.postForm('https://httpbin.org/post', { 'files[]': document.querySelector('#fileInput').files -}, { - headers: { - 'Content-Type': 'multipart/form-data' - } -}).then(({data})=> console.log(data)); +}); ``` -Axios supports the following shortcut methods: `postForm`, `putForm`, `patchForm` -which are just the corresponding http methods with the content-type header preset to `multipart/form-data`. - `FileList` object can be passed directly: ```js await axios.postForm('https://httpbin.org/post', document.querySelector('#fileInput').files) ``` -All files will be sent with the same field names: `files[]`; +All files will be sent with the same field names: `files[]`. -##### Manual FormData passing - -In node.js, you can use the [`form-data`](https://github.com/form-data/form-data) library as follows: +## πŸ†• HTML Form Posting (browser) + +Pass HTML Form element as a payload to submit it as `multipart/form-data` content. ```js -const FormData = require('form-data'); - -const form = new FormData(); -form.append('my_field', 'my value'); -form.append('my_buffer', new Buffer(10)); -form.append('my_file', fs.createReadStream('/foo/bar.jpg')); +await axios.postForm('https://httpbin.org/post', document.querySelector('#htmlForm')); +``` -axios.post('https://example.com', form) +`FormData` and `HTMLForm` objects can also be posted as `JSON` by explicitly setting the `Content-Type` header to `application/json`: + +```js +await axios.post('https://httpbin.org/post', document.querySelector('#htmlForm'), { + headers: { + 'Content-Type': 'application/json' + } +}) +``` + +For example, the Form + +```html +
+ + + + + + + + + +
``` +will be submitted as the following JSON object: + +```js +{ + "foo": "1", + "deep": { + "prop": { + "spaced": "3" + } + }, + "baz": [ + "4", + "5" + ], + "user": { + "age": "value2" + } +} +```` + +Sending `Blobs`/`Files` as JSON (`base64`) is not currently supported. + ## Semver Until axios reaches a `1.0` release, breaking changes will be released with a new minor version. For example `0.5.1`, and `0.5.4` will have the same API, but `0.6.0` will have breaking changes. diff --git a/bin/ssl_hotfix.js b/bin/ssl_hotfix.js new file mode 100644 index 0000000000..e79819f2e2 --- /dev/null +++ b/bin/ssl_hotfix.js @@ -0,0 +1,22 @@ +const {spawn} = require('child_process'); + +const args = process.argv.slice(2); + +console.log(`Running ${args.join(' ')} on ${process.version}\n`); + +const match = /v(\d+)/.exec(process.version); + +const isHotfixNeeded = match && match[1] > 16; + +isHotfixNeeded && console.warn('Setting --openssl-legacy-provider as ssl hotfix'); + +const test = spawn('cross-env', + isHotfixNeeded ? ['NODE_OPTIONS=--openssl-legacy-provider', ...args] : args, { + shell: true, + stdio: 'inherit' + } +); + +test.on('exit', function (code) { + process.exit(code) +}) diff --git a/index.d.ts b/index.d.ts index cd3a1f8577..bbf8390fb6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,15 @@ -// TypeScript Version: 3.0 -export type AxiosRequestHeaders = Record; +// TypeScript Version: 4.1 +type AxiosHeaders = Record; + +type MethodsHeaders = { + [Key in Method as Lowercase]: AxiosHeaders; +}; + +interface CommonHeaders { + common: AxiosHeaders; +} + +export type AxiosRequestHeaders = Partial; export type AxiosResponseHeaders = Record & { "set-cookie"?: string[] @@ -80,22 +90,40 @@ export interface GenericAbortSignal { } export interface FormDataVisitorHelpers { - defaultVisitor: FormDataVisitor; + defaultVisitor: SerializerVisitor; convertValue: (value: any) => any; isVisitable: (value: any) => boolean; } -export interface FormDataVisitor { - (value: any, key: string | number, path: null | Array, helpers: FormDataVisitorHelpers): boolean; +export interface SerializerVisitor { + ( + this: GenericFormData, + value: any, + key: string | number, + path: null | Array, + helpers: FormDataVisitorHelpers + ): boolean; } -export interface FormSerializerOptions { - visitor?: FormDataVisitor; +export interface SerializerOptions { + visitor?: SerializerVisitor; dots?: boolean; metaTokens?: boolean; indexes?: boolean; } +// tslint:disable-next-line +export interface FormSerializerOptions extends SerializerOptions { +} + +export interface ParamEncoder { + (value: any, defaultEncoder: (value: any) => any): any; +} + +export interface ParamsSerializerOptions extends SerializerOptions { + encode?: ParamEncoder; +} + export interface AxiosRequestConfig { url?: string; method?: Method | string; @@ -104,7 +132,7 @@ export interface AxiosRequestConfig { transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[]; headers?: AxiosRequestHeaders; params?: any; - paramsSerializer?: (params: any) => string; + paramsSerializer?: ParamsSerializerOptions; data?: D; timeout?: number; timeoutErrorMessage?: string; @@ -115,8 +143,8 @@ export interface AxiosRequestConfig { responseEncoding?: responseEncoding | string; xsrfCookieName?: string; xsrfHeaderName?: string; - onUploadProgress?: (progressEvent: any) => void; - onDownloadProgress?: (progressEvent: any) => void; + onUploadProgress?: (progressEvent: ProgressEvent) => void; + onDownloadProgress?: (progressEvent: ProgressEvent) => void; maxContentLength?: number; validateStatus?: ((status: number) => boolean) | null; maxBodyLength?: number; @@ -182,8 +210,9 @@ export class AxiosError extends Error { request?: any; response?: AxiosResponse; isAxiosError: boolean; - status?: string; + status?: number; toJSON: () => object; + cause?: Error; 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"; @@ -191,6 +220,8 @@ export class AxiosError extends Error { 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_NOT_SUPPORT = "ERR_NOT_SUPPORT"; + static readonly ERR_INVALID_URL = "ERR_INVALID_URL"; static readonly ERR_CANCELED = "ERR_CANCELED"; static readonly ECONNABORTED = "ECONNABORTED"; static readonly ETIMEDOUT = "ETIMEDOUT"; @@ -210,7 +241,7 @@ export interface Cancel { } export interface Canceler { - (message?: string): void; + (message?: string, config?: AxiosRequestConfig, request?: any): void; } export interface CancelTokenStatic { @@ -261,8 +292,8 @@ export class Axios { } export interface AxiosInstance extends Axios { - (config: AxiosRequestConfig): AxiosPromise; - (url: string, config?: AxiosRequestConfig): AxiosPromise; + , D = any>(config: AxiosRequestConfig): AxiosPromise; + , D = any>(url: string, config?: AxiosRequestConfig): AxiosPromise; defaults: Omit & { headers: HeadersDefaults & { @@ -275,6 +306,12 @@ export interface GenericFormData { append(name: string, value: any, options?: any): any; } +export interface GenericHTMLFormElement { + name: string; + method: string; + submit(): void; +} + export interface AxiosStatic extends AxiosInstance { create(config?: CreateAxiosDefaults): AxiosInstance; Cancel: CancelStatic; @@ -282,11 +319,12 @@ export interface AxiosStatic extends AxiosInstance { Axios: typeof Axios; AxiosError: typeof AxiosError; readonly VERSION: string; - isCancel(value: any): boolean; + isCancel(value: any): value is Cancel; all(values: Array>): Promise; spread(callback: (...args: T[]) => R): (array: T[]) => R; isAxiosError(payload: any): payload is AxiosError; toFormData(sourceObj: object, targetFormData?: GenericFormData, options?: FormSerializerOptions): GenericFormData; + formToJSON(form: GenericFormData|GenericHTMLFormElement): object; } declare const axios: AxiosStatic; diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 66aa3f27b1..fc872514c5 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -7,18 +7,32 @@ var buildURL = require('./../helpers/buildURL'); var getProxyForUrl = require('proxy-from-env').getProxyForUrl; var http = require('http'); var https = require('https'); -var httpFollow = require('follow-redirects').http; -var httpsFollow = require('follow-redirects').https; +var httpFollow = require('follow-redirects/http'); +var httpsFollow = require('follow-redirects/https'); var url = require('url'); var zlib = require('zlib'); var VERSION = require('./../env/data').version; var transitionalDefaults = require('../defaults/transitional'); var AxiosError = require('../core/AxiosError'); var CanceledError = require('../cancel/CanceledError'); +var platform = require('../platform'); +var fromDataURI = require('../helpers/fromDataURI'); +var stream = require('stream'); var isHttps = /https:?/; -var supportedProtocols = [ 'http:', 'https:', 'file:' ]; +var supportedProtocols = platform.protocols.map(function(protocol) { + return protocol + ':'; +}); + +function dispatchBeforeRedirect(options) { + if (options.beforeRedirects.proxy) { + options.beforeRedirects.proxy(options); + } + if (options.beforeRedirects.config) { + options.beforeRedirects.config(options); + } +} /** * @@ -59,7 +73,7 @@ function setProxy(options, configProxy, location) { } } - options.beforeRedirect = function beforeRedirect(redirectOptions) { + options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) { // Configure proxy for redirected request, passing the original config proxy to apply // the exact same logic as if the redirected request was performed by axios directly. setProxy(redirectOptions, configProxy, redirectOptions.href); @@ -90,6 +104,62 @@ module.exports = function httpAdapter(config) { rejectPromise(value); }; var data = config.data; + var responseType = config.responseType; + var responseEncoding = config.responseEncoding; + var method = config.method.toUpperCase(); + + // Parse url + var fullPath = buildFullPath(config.baseURL, config.url); + var parsed = url.parse(fullPath); + var protocol = parsed.protocol || supportedProtocols[0]; + + if (protocol === 'data:') { + var convertedData; + + if (method !== 'GET') { + return settle(resolve, reject, { + status: 405, + statusText: 'method not allowed', + headers: {}, + config: config + }); + } + + try { + convertedData = fromDataURI(config.url, responseType === 'blob', { + Blob: config.env && config.env.Blob + }); + } catch (err) { + throw AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config); + } + + if (responseType === 'text') { + convertedData = convertedData.toString(responseEncoding); + + if (!responseEncoding || responseEncoding === 'utf8') { + data = utils.stripBOM(convertedData); + } + } else if (responseType === 'stream') { + convertedData = stream.Readable.from(convertedData); + } + + return settle(resolve, reject, { + data: convertedData, + status: 200, + statusText: 'OK', + headers: {}, + config: config + }); + } + + if (supportedProtocols.indexOf(protocol) === -1) { + return reject(new AxiosError( + 'Unsupported protocol ' + protocol, + AxiosError.ERR_BAD_REQUEST, + config + )); + } + var headers = config.headers; var headerNames = {}; @@ -150,19 +220,6 @@ module.exports = function httpAdapter(config) { auth = username + ':' + password; } - // Parse url - var fullPath = buildFullPath(config.baseURL, config.url); - var parsed = url.parse(fullPath); - var protocol = parsed.protocol || supportedProtocols[0]; - - if (supportedProtocols.indexOf(protocol) === -1) { - return reject(new AxiosError( - 'Unsupported protocol ' + protocol, - AxiosError.ERR_BAD_REQUEST, - config - )); - } - if (!auth && parsed.auth) { var urlAuth = parsed.auth.split(':'); var urlUsername = urlAuth[0] || ''; @@ -186,11 +243,13 @@ module.exports = function httpAdapter(config) { var options = { path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), - method: config.method.toUpperCase(), + method: method, headers: headers, agents: { http: config.httpAgent, https: config.httpsAgent }, auth: auth, - protocol: protocol + protocol: protocol, + beforeRedirect: dispatchBeforeRedirect, + beforeRedirects: {} }; if (config.socketPath) { @@ -213,13 +272,16 @@ module.exports = function httpAdapter(config) { options.maxRedirects = config.maxRedirects; } if (config.beforeRedirect) { - options.beforeRedirect = config.beforeRedirect; + options.beforeRedirects.config = config.beforeRedirect; } transport = isHttpsRequest ? httpsFollow : httpFollow; } if (config.maxBodyLength > -1) { options.maxBodyLength = config.maxBodyLength; + } else { + // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited + options.maxBodyLength = Infinity; } if (config.insecureHTTPParser) { @@ -231,7 +293,7 @@ module.exports = function httpAdapter(config) { if (req.aborted) return; // uncompress the response body transparently if required - var stream = res; + var responseStream = res; // return the last request in case of redirects var lastRequest = res.req || req; @@ -250,7 +312,7 @@ module.exports = function httpAdapter(config) { case 'compress': case 'deflate': // add the unzipper to the body stream processing pipeline - stream = stream.pipe(zlib.createUnzip()); + responseStream = responseStream.pipe(zlib.createUnzip()); // remove the content-encoding in order to not confuse downstream operations delete res.headers['content-encoding']; @@ -266,13 +328,13 @@ module.exports = function httpAdapter(config) { request: lastRequest }; - if (config.responseType === 'stream') { - response.data = stream; + if (responseType === 'stream') { + response.data = responseStream; settle(resolve, reject, response); } else { var responseBuffer = []; var totalResponseBytes = 0; - stream.on('data', function handleStreamData(chunk) { + responseStream.on('data', function handleStreamData(chunk) { responseBuffer.push(chunk); totalResponseBytes += chunk.length; @@ -280,17 +342,17 @@ module.exports = function httpAdapter(config) { if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) { // stream.destroy() emit aborted event before calling reject() on Node.js v16 rejected = true; - stream.destroy(); + responseStream.destroy(); reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, lastRequest)); } }); - stream.on('aborted', function handlerStreamAborted() { + responseStream.on('aborted', function handlerStreamAborted() { if (rejected) { return; } - stream.destroy(); + responseStream.destroy(); reject(new AxiosError( 'maxContentLength size of ' + config.maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, @@ -299,17 +361,17 @@ module.exports = function httpAdapter(config) { )); }); - stream.on('error', function handleStreamError(err) { + responseStream.on('error', function handleStreamError(err) { if (req.aborted) return; reject(AxiosError.from(err, null, config, lastRequest)); }); - stream.on('end', function handleStreamEnd() { + responseStream.on('end', function handleStreamEnd() { try { var responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer); - if (config.responseType !== 'arraybuffer') { - responseData = responseData.toString(config.responseEncoding); - if (!config.responseEncoding || config.responseEncoding === 'utf8') { + if (responseType !== 'arraybuffer') { + responseData = responseData.toString(responseEncoding); + if (!responseEncoding || responseEncoding === 'utf8') { responseData = utils.stripBOM(responseData); } } @@ -358,9 +420,13 @@ 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(); + var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; var transitional = config.transitional || transitionalDefaults; + if (config.timeoutErrorMessage) { + timeoutErrorMessage = config.timeoutErrorMessage; + } reject(new AxiosError( - 'timeout of ' + timeout + 'ms exceeded', + timeoutErrorMessage, transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, req diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index c22783f179..8612a77802 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -11,6 +11,7 @@ var transitionalDefaults = require('../defaults/transitional'); var AxiosError = require('../core/AxiosError'); var CanceledError = require('../cancel/CanceledError'); var parseProtocol = require('../helpers/parseProtocol'); +var platform = require('../platform'); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { @@ -210,7 +211,7 @@ module.exports = function xhrAdapter(config) { var protocol = parseProtocol(fullPath); - if (protocol && [ 'http', 'https', 'file', 'blob' ].indexOf(protocol) === -1) { + if (protocol && platform.protocols.indexOf(protocol) === -1) { reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config)); return; } diff --git a/lib/axios.js b/lib/axios.js index cf6583cfe5..bbbe301bf9 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -5,7 +5,7 @@ var bind = require('./helpers/bind'); var Axios = require('./core/Axios'); var mergeConfig = require('./core/mergeConfig'); var defaults = require('./defaults'); - +var formDataToJSON = require('./helpers/formDataToJSON'); /** * Create an instance of Axios * @@ -58,6 +58,10 @@ axios.spread = require('./helpers/spread'); // Expose isAxiosError axios.isAxiosError = require('./helpers/isAxiosError'); +axios.formToJSON = function(thing) { + return formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing); +}; + module.exports = axios; // Allow use of default import syntax in TypeScript diff --git a/lib/cancel/CancelToken.js b/lib/cancel/CancelToken.js index 07ec10e3b2..c2fe64c7fd 100644 --- a/lib/cancel/CancelToken.js +++ b/lib/cancel/CancelToken.js @@ -49,13 +49,13 @@ function CancelToken(executor) { return promise; }; - executor(function cancel(message) { + executor(function cancel(message, config, request) { if (token.reason) { // Cancellation has already been requested return; } - token.reason = new CanceledError(message); + token.reason = new CanceledError(message, config, request); resolvePromise(token.reason); }); } diff --git a/lib/core/Axios.js b/lib/core/Axios.js index 3a79171099..ef7e810129 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -25,7 +25,8 @@ function Axios(instanceConfig) { /** * Dispatch a request * - * @param {Object} config The config specific for this request (merged with this.defaults) + * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults) + * @param {?Object} config */ Axios.prototype.request = function request(configOrUrl, config) { /*eslint no-param-reassign:0*/ diff --git a/lib/core/AxiosError.js b/lib/core/AxiosError.js index 2125a4e6aa..291dc33c49 100644 --- a/lib/core/AxiosError.js +++ b/lib/core/AxiosError.js @@ -16,7 +16,9 @@ function AxiosError(message, code, config, request, response) { Error.call(this); if (Error.captureStackTrace) { - Error.captureStackTrace(this); + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = (new Error()).stack; } this.message = message; @@ -62,7 +64,9 @@ var descriptors = {}; 'ERR_DEPRECATED', 'ERR_BAD_RESPONSE', 'ERR_BAD_REQUEST', - 'ERR_CANCELED' + 'ERR_CANCELED', + 'ERR_NOT_SUPPORT', + 'ERR_INVALID_URL' // eslint-disable-next-line func-names ].forEach(function(code) { descriptors[code] = {value: code}; @@ -81,6 +85,8 @@ AxiosError.from = function(error, code, config, request, response, customProps) AxiosError.call(axiosError, error.message, code, config, request, response); + axiosError.cause = error; + axiosError.name = error.name; customProps && Object.assign(axiosError, customProps); diff --git a/lib/core/dispatchRequest.js b/lib/core/dispatchRequest.js index 997d4c952e..6ac7bc0e42 100644 --- a/lib/core/dispatchRequest.js +++ b/lib/core/dispatchRequest.js @@ -5,6 +5,7 @@ var transformData = require('./transformData'); var isCancel = require('../cancel/isCancel'); var defaults = require('../defaults'); var CanceledError = require('../cancel/CanceledError'); +var normalizeHeaderName = require('../helpers/normalizeHeaderName'); /** * Throws a `CanceledError` if cancellation has been requested. @@ -40,6 +41,9 @@ module.exports = function dispatchRequest(config) { config.transformRequest ); + normalizeHeaderName(config.headers, 'Accept'); + normalizeHeaderName(config.headers, 'Content-Type'); + // Flatten headers config.headers = utils.merge( config.headers.common || {}, diff --git a/lib/defaults/index.js b/lib/defaults/index.js index 1be97ffd22..d7ed025937 100644 --- a/lib/defaults/index.js +++ b/lib/defaults/index.js @@ -5,6 +5,9 @@ var normalizeHeaderName = require('../helpers/normalizeHeaderName'); var AxiosError = require('../core/AxiosError'); var transitionalDefaults = require('./transitional'); var toFormData = require('../helpers/toFormData'); +var toURLEncodedForm = require('../helpers/toURLEncodedForm'); +var platform = require('../platform'); +var formDataToJSON = require('../helpers/formDataToJSON'); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -53,8 +56,24 @@ var defaults = { normalizeHeaderName(headers, 'Accept'); normalizeHeaderName(headers, 'Content-Type'); - if (utils.isFormData(data) || - utils.isArrayBuffer(data) || + var contentType = headers && headers['Content-Type'] || ''; + var hasJSONContentType = contentType.indexOf('application/json') > -1; + var isObjectPayload = utils.isObject(data); + + if (isObjectPayload && utils.isHTMLForm(data)) { + data = new FormData(data); + } + + var isFormData = utils.isFormData(data); + + if (isFormData) { + if (!hasJSONContentType) { + return data; + } + return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data; + } + + if (utils.isArrayBuffer(data) || utils.isBuffer(data) || utils.isStream(data) || utils.isFile(data) || @@ -70,19 +89,25 @@ var defaults = { return data.toString(); } - var isObjectPayload = utils.isObject(data); - var contentType = headers && headers['Content-Type']; - var isFileList; - if ((isFileList = utils.isFileList(data)) || (isObjectPayload && contentType === 'multipart/form-data')) { - var _FormData = this.env && this.env.FormData; - return toFormData( - isFileList ? {'files[]': data} : data, - _FormData && new _FormData(), - this.formSerializer - ); - } else if (isObjectPayload || contentType === 'application/json') { + if (isObjectPayload) { + if (contentType.indexOf('application/x-www-form-urlencoded') !== -1) { + return toURLEncodedForm(data, this.formSerializer).toString(); + } + + if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) { + var _FormData = this.env && this.env.FormData; + + return toFormData( + isFileList ? {'files[]': data} : data, + _FormData && new _FormData(), + this.formSerializer + ); + } + } + + if (isObjectPayload || hasJSONContentType ) { setContentTypeIfUnset(headers, 'application/json'); return stringifySafely(data); } @@ -92,11 +117,13 @@ var defaults = { transformResponse: [function transformResponse(data) { var transitional = this.transitional || defaults.transitional; - var silentJSONParsing = transitional && transitional.silentJSONParsing; var forcedJSONParsing = transitional && transitional.forcedJSONParsing; - var strictJSONParsing = !silentJSONParsing && this.responseType === 'json'; + var JSONRequested = this.responseType === 'json'; + + if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) { + var silentJSONParsing = transitional && transitional.silentJSONParsing; + var strictJSONParsing = !silentJSONParsing && JSONRequested; - if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) { try { return JSON.parse(data); } catch (e) { @@ -125,7 +152,8 @@ var defaults = { maxBodyLength: -1, env: { - FormData: require('./env/FormData') + FormData: platform.classes.FormData, + Blob: platform.classes.Blob }, validateStatus: function validateStatus(status) { diff --git a/lib/defaults/env/FormData.js b/lib/env/classes/FormData.js similarity index 100% rename from lib/defaults/env/FormData.js rename to lib/env/classes/FormData.js diff --git a/lib/helpers/AxiosURLSearchParams.js b/lib/helpers/AxiosURLSearchParams.js new file mode 100644 index 0000000000..7c49f185f7 --- /dev/null +++ b/lib/helpers/AxiosURLSearchParams.js @@ -0,0 +1,42 @@ +'use strict'; + +var toFormData = require('./toFormData'); + +function encode(str) { + var charMap = { + '!': '%21', + "'": '%27', + '(': '%28', + ')': '%29', + '~': '%7E', + '%20': '+', + '%00': '\x00' + }; + return encodeURIComponent(str).replace(/[!'\(\)~]|%20|%00/g, function replacer(match) { + return charMap[match]; + }); +} + +function AxiosURLSearchParams(params, options) { + this._pairs = []; + + params && toFormData(params, this, options); +} + +var prototype = AxiosURLSearchParams.prototype; + +prototype.append = function append(name, value) { + this._pairs.push([name, value]); +}; + +prototype.toString = function toString(encoder) { + var _encode = encoder ? function(value) { + return encoder.call(this, value, encode); + } : encode; + + return this._pairs.map(function each(pair) { + return _encode(pair[0]) + '=' + _encode(pair[1]); + }, '').join('&'); +}; + +module.exports = AxiosURLSearchParams; diff --git a/lib/helpers/buildURL.js b/lib/helpers/buildURL.js index 31595c33a8..bebacb3aee 100644 --- a/lib/helpers/buildURL.js +++ b/lib/helpers/buildURL.js @@ -1,6 +1,7 @@ 'use strict'; -var utils = require('./../utils'); +var utils = require('../utils'); +var AxiosURLSearchParams = require('../helpers/AxiosURLSearchParams'); function encode(val) { return encodeURIComponent(val). @@ -17,53 +18,29 @@ function encode(val) { * * @param {string} url The base of the url (e.g., http://www.google.com) * @param {object} [params] The params to be appended + * @param {?object} options * @returns {string} The formatted url */ -module.exports = function buildURL(url, params, paramsSerializer) { +module.exports = function buildURL(url, params, options) { /*eslint no-param-reassign:0*/ if (!params) { return url; } - var serializedParams; - if (paramsSerializer) { - serializedParams = paramsSerializer(params); - } else if (utils.isURLSearchParams(params)) { - serializedParams = params.toString(); - } else { - var parts = []; + var hashmarkIndex = url.indexOf('#'); - utils.forEach(params, function serialize(val, key) { - if (val === null || typeof val === 'undefined') { - return; - } - - if (utils.isArray(val)) { - key = key + '[]'; - } else { - val = [val]; - } - - utils.forEach(val, function parseValue(v) { - if (utils.isDate(v)) { - v = v.toISOString(); - } else if (utils.isObject(v)) { - v = JSON.stringify(v); - } - parts.push(encode(key) + '=' + encode(v)); - }); - }); - - serializedParams = parts.join('&'); + if (hashmarkIndex !== -1) { + url = url.slice(0, hashmarkIndex); } - if (serializedParams) { - var hashmarkIndex = url.indexOf('#'); - if (hashmarkIndex !== -1) { - url = url.slice(0, hashmarkIndex); - } + var _encode = options && options.encode || encode; + + var serializerParams = utils.isURLSearchParams(params) ? + params.toString() : + new AxiosURLSearchParams(params, options).toString(_encode); - url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; + if (serializerParams) { + url += (url.indexOf('?') === -1 ? '?' : '&') + serializerParams; } return url; diff --git a/lib/helpers/formDataToJSON.js b/lib/helpers/formDataToJSON.js new file mode 100644 index 0000000000..45a10364e4 --- /dev/null +++ b/lib/helpers/formDataToJSON.js @@ -0,0 +1,71 @@ +'use strict'; + +var utils = require('../utils'); + +function parsePropPath(name) { + // foo[x][y][z] + // foo.x.y.z + // foo-x-y-z + // foo x y z + return utils.matchAll(/\w+|\[(\w*)]/g, name).map(function(match) { + return match[0] === '[]' ? '' : match[1] || match[0]; + }); +} + +function arrayToObject(arr) { + var obj = {}; + var keys = Object.keys(arr); + var i; + var len = keys.length; + var key; + for (i = 0; i < len; i++) { + key = keys[i]; + obj[key] = arr[key]; + } + return obj; +} + +function formDataToJSON(formData) { + function buildPath(path, value, target, index) { + var name = path[index++]; + var isNumericKey = Number.isFinite(+name); + var isLast = index >= path.length; + name = !name && utils.isArray(target) ? target.length : name; + + if (isLast) { + if (utils.hasOwnProperty(target, name)) { + target[name] = [target[name], value]; + } else { + target[name] = value; + } + + return !isNumericKey; + } + + if (!target[name] || !utils.isObject(target[name])) { + target[name] = []; + } + + var result = buildPath(path, value, target[name], index); + + if (result && utils.isArray(target[name])) { + target[name] = arrayToObject(target[name]); + } + + return !isNumericKey; + } + + if (utils.isFormData(formData) && utils.isFunction(formData.entries)) { + var obj = {}; + + utils.forEachEntry(formData, function(name, value) { + buildPath(parsePropPath(name), value, obj, 0); + }); + + return obj; + } + + return null; +} + +module.exports = formDataToJSON; diff --git a/lib/helpers/fromDataURI.js b/lib/helpers/fromDataURI.js new file mode 100644 index 0000000000..3ee2ab925a --- /dev/null +++ b/lib/helpers/fromDataURI.js @@ -0,0 +1,51 @@ +'use strict'; + +var AxiosError = require('../core/AxiosError'); +var parseProtocol = require('./parseProtocol'); +var platform = require('../platform'); + +var DATA_URL_PATTERN = /^(?:([^;]+);)?(?:[^;]+;)?(base64|),([\s\S]*)$/; + +/** + * Parse data uri to a Buffer or Blob + * @param {String} uri + * @param {?Boolean} asBlob + * @param {?Object} options + * @param {?Function} options.Blob + * @returns {Buffer|Blob} + */ +module.exports = function fromDataURI(uri, asBlob, options) { + var _Blob = options && options.Blob || platform.classes.Blob; + var protocol = parseProtocol(uri); + + if (asBlob === undefined && _Blob) { + asBlob = true; + } + + if (protocol === 'data') { + uri = protocol.length ? uri.slice(protocol.length + 1) : uri; + + var match = DATA_URL_PATTERN.exec(uri); + + if (!match) { + throw new AxiosError('Invalid URL', AxiosError.ERR_INVALID_URL); + } + + var mime = match[1]; + var isBase64 = match[2]; + var body = match[3]; + var buffer = Buffer.from(decodeURIComponent(body), isBase64 ? 'base64' : 'utf8'); + + if (asBlob) { + if (!_Blob) { + throw new AxiosError('Blob is not supported', AxiosError.ERR_NOT_SUPPORT); + } + + return new _Blob([buffer], {type: mime}); + } + + return buffer; + } + + throw new AxiosError('Unsupported protocol ' + protocol, AxiosError.ERR_NOT_SUPPORT); +}; diff --git a/lib/helpers/toFormData.js b/lib/helpers/toFormData.js index 9cfdbd76a6..6dcb9d2337 100644 --- a/lib/helpers/toFormData.js +++ b/lib/helpers/toFormData.js @@ -1,7 +1,8 @@ 'use strict'; var utils = require('../utils'); -var envFormData = require('../defaults/env/FormData'); +var AxiosError = require('../core/AxiosError'); +var envFormData = require('../env/classes/FormData'); function isVisitable(thing) { return utils.isPlainObject(thing) || utils.isArray(thing); @@ -28,6 +29,10 @@ var predicates = utils.toFlatObject(utils, {}, null, function filter(prop) { return /^is[A-Z]/.test(prop); }); +function isSpecCompliant(thing) { + return thing && utils.isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]; +} + /** * Convert a data object to FormData * @param {Object} obj @@ -41,6 +46,10 @@ var predicates = utils.toFlatObject(utils, {}, null, function filter(prop) { **/ function toFormData(obj, formData, options) { + if (!utils.isObject(obj)) { + throw new TypeError('target must be an object'); + } + // eslint-disable-next-line no-param-reassign formData = formData || new (envFormData || FormData)(); @@ -59,6 +68,8 @@ function toFormData(obj, formData, options) { var visitor = options.visitor || defaultVisitor; var dots = options.dots; var indexes = options.indexes; + var _Blob = options.Blob || typeof Blob !== 'undefined' && Blob; + var useBlob = _Blob && isSpecCompliant(formData); if (!utils.isFunction(visitor)) { throw new TypeError('visitor must be a function'); @@ -71,14 +82,17 @@ function toFormData(obj, formData, options) { return value.toISOString(); } + if (!useBlob && utils.isBlob(value)) { + throw new AxiosError('Blob is not supported. Use a Buffer instead.'); + } + if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) { - return typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value); + return useBlob && typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value); } return value; } - /** * * @param {*} value @@ -88,7 +102,7 @@ function toFormData(obj, formData, options) { * @returns {boolean} return true to visit the each prop of the value recursively */ function defaultVisitor(value, key, path) { - var arr; + var arr = value; if (value && !path && typeof value === 'object') { if (utils.endsWith(key, '{}')) { @@ -96,7 +110,10 @@ function toFormData(obj, formData, options) { key = metaTokens ? key : key.slice(0, -2); // eslint-disable-next-line no-param-reassign value = JSON.stringify(value); - } else if (!utils.isPlainObject(value) && (arr = utils.toArray(value)) && isFlatArray(arr)) { + } else if ( + (utils.isArray(value) && isFlatArray(value)) || + (utils.isFileList(value) || utils.endsWith(key, '[]') && (arr = utils.toArray(value)) + )) { // eslint-disable-next-line no-param-reassign key = removeBrackets(key); @@ -138,7 +155,7 @@ function toFormData(obj, formData, options) { stack.push(value); utils.forEach(value, function each(el, key) { - var result = !utils.isUndefined(el) && defaultVisitor.call( + var result = !utils.isUndefined(el) && visitor.call( formData, el, utils.isString(key) ? key.trim() : key, path, exposedHelpers ); @@ -150,8 +167,8 @@ function toFormData(obj, formData, options) { stack.pop(); } - if (!utils.isPlainObject(obj)) { - throw new TypeError('data must be a plain object'); + if (!utils.isObject(obj)) { + throw new TypeError('data must be an object'); } build(obj); diff --git a/lib/helpers/toURLEncodedForm.js b/lib/helpers/toURLEncodedForm.js new file mode 100644 index 0000000000..947a39a6ce --- /dev/null +++ b/lib/helpers/toURLEncodedForm.js @@ -0,0 +1,18 @@ +'use strict'; + +var utils = require('../utils'); +var toFormData = require('./toFormData'); +var platform = require('../platform/'); + +module.exports = function toURLEncodedForm(data, options) { + return toFormData(data, new platform.classes.URLSearchParams(), Object.assign({ + visitor: function(value, key, path, helpers) { + if (platform.isNode && utils.isBuffer(value)) { + this.append(key, value.toString('base64')); + return false; + } + + return helpers.defaultVisitor.apply(this, arguments); + } + }, options)); +}; diff --git a/lib/platform/browser/classes/FormData.js b/lib/platform/browser/classes/FormData.js new file mode 100644 index 0000000000..6af83c9fda --- /dev/null +++ b/lib/platform/browser/classes/FormData.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = FormData; diff --git a/lib/platform/browser/classes/URLSearchParams.js b/lib/platform/browser/classes/URLSearchParams.js new file mode 100644 index 0000000000..65f63d8827 --- /dev/null +++ b/lib/platform/browser/classes/URLSearchParams.js @@ -0,0 +1,5 @@ +'use strict'; + +var AxiosURLSearchParams = require('../../../helpers/AxiosURLSearchParams'); + +module.exports = typeof URLSearchParams !== 'undefined' ? URLSearchParams : AxiosURLSearchParams; diff --git a/lib/platform/browser/index.js b/lib/platform/browser/index.js new file mode 100644 index 0000000000..c254a1e38d --- /dev/null +++ b/lib/platform/browser/index.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + isBrowser: true, + classes: { + URLSearchParams: require('./classes/URLSearchParams'), + FormData: require('./classes/FormData'), + Blob: Blob + }, + protocols: ['http', 'https', 'file', 'blob', 'url'] +}; diff --git a/lib/platform/index.js b/lib/platform/index.js new file mode 100644 index 0000000000..8560532753 --- /dev/null +++ b/lib/platform/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./node/'); diff --git a/lib/platform/node/classes/FormData.js b/lib/platform/node/classes/FormData.js new file mode 100644 index 0000000000..a186bc0bfd --- /dev/null +++ b/lib/platform/node/classes/FormData.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('form-data'); diff --git a/lib/platform/node/classes/URLSearchParams.js b/lib/platform/node/classes/URLSearchParams.js new file mode 100644 index 0000000000..1ae3fc58df --- /dev/null +++ b/lib/platform/node/classes/URLSearchParams.js @@ -0,0 +1,5 @@ +'use strict'; + +var url = require('url'); + +module.exports = url.URLSearchParams; diff --git a/lib/platform/node/index.js b/lib/platform/node/index.js new file mode 100644 index 0000000000..b41ff36f38 --- /dev/null +++ b/lib/platform/node/index.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + isNode: true, + classes: { + URLSearchParams: require('./classes/URLSearchParams'), + FormData: require('./classes/FormData'), + Blob: typeof Blob !== 'undefined' && Blob || null + }, + protocols: [ 'http', 'https', 'file', 'data' ] +}; diff --git a/lib/utils.js b/lib/utils.js index 1ecd51ee09..577462f924 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -229,15 +229,16 @@ function trim(str) { * navigator.product -> 'NativeScript' or 'NS' */ function isStandardBrowserEnv() { - if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || - navigator.product === 'NativeScript' || - navigator.product === 'NS')) { + var product; + if (typeof navigator !== 'undefined' && ( + (product = navigator.product) === 'ReactNative' || + product === 'NativeScript' || + product === 'NS') + ) { return false; } - return ( - typeof window !== 'undefined' && - typeof document !== 'undefined' - ); + + return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** @@ -440,6 +441,38 @@ var isTypedArray = (function(TypedArray) { }; })(typeof Uint8Array !== 'undefined' && Object.getPrototypeOf(Uint8Array)); +function forEachEntry(obj, fn) { + var generator = obj && obj[Symbol.iterator]; + + var iterator = generator.call(obj); + + var result; + + while ((result = iterator.next()) && !result.done) { + var pair = result.value; + fn.call(obj, pair[0], pair[1]); + } +} + +function matchAll(regExp, str) { + var matches; + var arr = []; + + while ((matches = regExp.exec(str)) !== null) { + arr.push(matches); + } + + return arr; +} + +var isHTMLForm = kindOfTest('HTMLFormElement'); + +var hasOwnProperty = (function resolver(_hasOwnProperty) { + return function(obj, prop) { + return _hasOwnProperty.call(obj, prop); + }; +})(Object.prototype.hasOwnProperty); + module.exports = { isArray: isArray, isArrayBuffer: isArrayBuffer, @@ -470,5 +503,9 @@ module.exports = { endsWith: endsWith, toArray: toArray, isTypedArray: isTypedArray, - isFileList: isFileList + isFileList: isFileList, + forEachEntry: forEachEntry, + matchAll: matchAll, + isHTMLForm: isHTMLForm, + hasOwnProperty: hasOwnProperty }; diff --git a/package-lock.json b/package-lock.json index e537d68521..bdb974ece4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "follow-redirects": "^1.15.0", - "form-data": "^4.0.0" + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" }, "devDependencies": { "@rollup/plugin-babel": "^5.3.0", @@ -19,7 +20,9 @@ "@rollup/plugin-multi-entry": "^4.0.0", "@rollup/plugin-node-resolve": "^9.0.0", "abortcontroller-polyfill": "^1.7.3", + "body-parser": "^1.20.0", "coveralls": "^3.1.1", + "cross-env": "^7.0.3", "dtslint": "^4.2.1", "es6-promise": "^4.2.8", "express": "^4.18.1", @@ -3813,6 +3816,24 @@ "sha.js": "^2.4.8" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7036,9 +7057,9 @@ } }, "node_modules/grunt": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.2.tgz", - "integrity": "sha512-XCtfaIu72OyDqK24MjWiGC9SwlkuhkS1mrULr1xzuJ2XqAFhP3ZAchZGHJeSCY6mkaOXU4F7SbmmCF7xIVoC9w==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", + "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", "dev": true, "dependencies": { "dateformat": "~3.0.3", @@ -11989,8 +12010,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/prr": { "version": "1.0.1", @@ -20214,6 +20234,15 @@ "sha.js": "^2.4.8" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -22851,9 +22880,9 @@ "dev": true }, "grunt": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.2.tgz", - "integrity": "sha512-XCtfaIu72OyDqK24MjWiGC9SwlkuhkS1mrULr1xzuJ2XqAFhP3ZAchZGHJeSCY6mkaOXU4F7SbmmCF7xIVoC9w==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", + "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", "dev": true, "requires": { "dateformat": "~3.0.3", @@ -26801,8 +26830,7 @@ "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "prr": { "version": "1.0.1", diff --git a/package.json b/package.json index c262de8feb..7f08ba5892 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "main": "index.js", "types": "index.d.ts", "scripts": { - "test": "grunt test && dtslint", + "test": "node bin/ssl_hotfix.js grunt test && node bin/ssl_hotfix.js dtslint", "start": "node ./sandbox/server.js", "preversion": "grunt version && npm test", - "build": "NODE_ENV=production grunt build", + "build": "cross-env NODE_ENV=production grunt build", "examples": "node ./examples/server.js", "coveralls": "cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", "fix": "eslint --fix lib/**/*.js" @@ -37,7 +37,9 @@ "@rollup/plugin-multi-entry": "^4.0.0", "@rollup/plugin-node-resolve": "^9.0.0", "abortcontroller-polyfill": "^1.7.3", + "body-parser": "^1.20.0", "coveralls": "^3.1.1", + "cross-env": "^7.0.3", "dtslint": "^4.2.1", "es6-promise": "^4.2.8", "express": "^4.18.1", @@ -79,7 +81,7 @@ }, "browser": { "./lib/adapters/http.js": "./lib/adapters/xhr.js", - "./lib/defaults/env/FormData.js": "./lib/helpers/null.js" + "./lib/platform/node/index.js": "./lib/platform/browser/index.js" }, "jsdelivr": "dist/axios.min.js", "unpkg": "dist/axios.min.js", diff --git a/test/specs/helpers/buildURL.spec.js b/test/specs/helpers/buildURL.spec.js index 7adf57449e..0bee3e9a05 100644 --- a/test/specs/helpers/buildURL.spec.js +++ b/test/specs/helpers/buildURL.spec.js @@ -17,7 +17,7 @@ describe('helpers::buildURL', function () { foo: { bar: 'baz' } - })).toEqual('/foo?foo=' + encodeURI('{"bar":"baz"}')); + })).toEqual('/foo?foo[bar]=baz'); }); it('should support date params', function () { @@ -60,15 +60,6 @@ describe('helpers::buildURL', function () { })).toEqual('/foo?foo=bar&query=baz'); }); - it('should use serializer if provided', function () { - serializer = sinon.stub(); - params = {foo: 'bar'}; - serializer.returns('foo=bar'); - expect(buildURL('/foo', params, serializer)).toEqual('/foo?foo=bar'); - expect(serializer.calledOnce).toBe(true); - expect(serializer.calledWith(params)).toBe(true); - }); - it('should support URLSearchParams', function () { expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toEqual('/foo?bar=baz'); }); diff --git a/test/specs/helpers/formDataToJSON.spec.js b/test/specs/helpers/formDataToJSON.spec.js new file mode 100644 index 0000000000..69ea1a6d51 --- /dev/null +++ b/test/specs/helpers/formDataToJSON.spec.js @@ -0,0 +1,50 @@ +var formDataToJSON = require('../../../lib/helpers/formDataToJSON'); + +describe('formDataToJSON', function () { + it('should convert a FormData Object to JSON Object', function () { + const formData = new FormData(); + + formData.append('foo[bar][baz]', '123'); + + expect(formDataToJSON(formData)).toEqual({ + foo: { + bar: { + baz: '123' + } + } + }); + }); + + it('should convert repeatable values as an array', function () { + const formData = new FormData(); + + formData.append('foo', '1'); + formData.append('foo', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'] + }); + }); + + it('should convert props with empty brackets to arrays', function () { + const formData = new FormData(); + + formData.append('foo[]', '1'); + formData.append('foo[]', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'] + }); + }); + + it('should supported indexed arrays', function () { + const formData = new FormData(); + + formData.append('foo[0]', '1'); + formData.append('foo[1]', '2'); + + expect(formDataToJSON(formData)).toEqual({ + foo: ['1', '2'] + }); + }); +}); diff --git a/test/specs/instance.spec.js b/test/specs/instance.spec.js index e1b9a7c9d3..e29c2f7109 100644 --- a/test/specs/instance.spec.js +++ b/test/specs/instance.spec.js @@ -25,7 +25,8 @@ describe('instance', function () { 'isAxiosError', 'VERSION', 'default', - 'toFormData' + 'toFormData', + 'formToJSON' ].indexOf(prop) > -1) { continue; } diff --git a/test/specs/transform.spec.js b/test/specs/transform.spec.js index f7e62cb957..1ae052f5b9 100644 --- a/test/specs/transform.spec.js +++ b/test/specs/transform.spec.js @@ -177,4 +177,24 @@ describe('transform', function () { done(); }); }); + + it('should normalize \'content-type\' header when using a custom transformRequest', function (done) { + var data = { + foo: 'bar' + }; + + axios.post('/foo', data, { + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + transformRequest: [ + function () { + return 'aa=44' + } + ] + }); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders['Content-Type']).toEqual('application/x-www-form-urlencoded'); + done(); + }); + }); }); diff --git a/test/typescript/axios.ts b/test/typescript/axios.ts index 94d41602c1..c0c61599e1 100644 --- a/test/typescript/axios.ts +++ b/test/typescript/axios.ts @@ -20,7 +20,10 @@ const config: AxiosRequestConfig = { ], headers: { 'X-FOO': 'bar' }, params: { id: 12345 }, - paramsSerializer: (params: any) => 'id=12345', + paramsSerializer: { + indexes: true, + encode: (value) => value + }, data: { foo: 'bar' }, timeout: 10000, withCredentials: true, diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index c92e0afd2d..ed3db64b5f 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -12,8 +12,12 @@ var server, proxy; var AxiosError = require('../../../lib/core/AxiosError'); var FormData = require('form-data'); var formidable = require('formidable'); -const express = require('express'); -const multer = require('multer'); +var express = require('express'); +var multer = require('multer'); +var bodyParser = require('body-parser'); +const isBlobSupported = typeof Blob !== 'undefined'; + +var noop = ()=> {}; describe('supports http with nodejs', function () { @@ -142,7 +146,7 @@ describe('supports http with nodejs', function () { assert.strictEqual(success, false, 'request should not succeed'); assert.strictEqual(failure, true, 'request should fail'); assert.strictEqual(error.code, 'ECONNABORTED'); - assert.strictEqual(error.message, 'timeout of 250ms exceeded'); + assert.strictEqual(error.message, 'oops, timeout'); done(); }, 300); }); @@ -204,7 +208,7 @@ describe('supports http with nodejs', function () { assert.equal(res.data, str); assert.equal(res.request.path, '/two'); done(); - }).catch(done);; + }).catch(done); }); }); @@ -223,7 +227,7 @@ describe('supports http with nodejs', function () { assert.equal(res.status, 302); assert.equal(res.headers['location'], '/foo'); done(); - }).catch(done);; + }).catch(done); }); }); @@ -241,7 +245,7 @@ describe('supports http with nodejs', function () { assert.equal(error.code, AxiosError.ERR_FR_TOO_MANY_REDIRECTS); assert.equal(error.message, 'Maximum number of redirects exceeded'); done(); - }); + }).catch(done); }); }); @@ -263,6 +267,56 @@ describe('supports http with nodejs', function () { }).catch(function (error) { assert.equal(error.message, 'Provided path is not allowed'); done(); + }).catch(done); + }); + }); + + it('should support beforeRedirect and proxy with redirect', function (done) { + var requestCount = 0; + var totalRedirectCount = 5; + server = http.createServer(function (req, res) { + requestCount += 1; + if (requestCount <= totalRedirectCount) { + res.setHeader('Location', 'http://localhost:4444'); + res.writeHead(302); + } + res.end(); + }).listen(4444, function () { + var proxyUseCount = 0; + proxy = http.createServer(function (request, response) { + proxyUseCount += 1; + var parsed = url.parse(request.url); + var opts = { + host: parsed.hostname, + port: parsed.port, + path: parsed.path + }; + + http.get(opts, function (res) { + response.writeHead(res.statusCode, res.headers); + res.on('data', function (data) { + response.write(data) + }); + res.on('end', function () { + response.end(); + }); + }); + }).listen(4000, function () { + var configBeforeRedirectCount = 0; + axios.get('http://localhost:4444/', { + proxy: { + host: 'localhost', + port: 4000 + }, + maxRedirects: totalRedirectCount, + beforeRedirect: function (options) { + configBeforeRedirectCount += 1; + } + }).then(function (res) { + assert.equal(totalRedirectCount, configBeforeRedirectCount, 'should invoke config.beforeRedirect option on every redirect'); + assert.equal(totalRedirectCount + 1, proxyUseCount, 'should go through proxy on every redirect'); + done(); + }).catch(done); }); }); }); @@ -515,6 +569,32 @@ describe('supports http with nodejs', function () { }); }); + it('should properly support default max body length (follow-redirects as well)', function (done) { + // taken from https://github.com/follow-redirects/follow-redirects/blob/22e81fc37132941fb83939d1dc4c2282b5c69521/index.js#L461 + var followRedirectsMaxBodyDefaults = 10 * 1024 *1024; + var data = Array(2 * followRedirectsMaxBodyDefaults).join('ΠΆ'); + + server = http.createServer(function (req, res) { + // consume the req stream + req.on('data', noop); + // and wait for the end before responding, otherwise an ECONNRESET error will be thrown + req.on('end', ()=> { + res.end('OK'); + }); + }).listen(4444, function (err) { + if (err) { + return done(err); + } + // send using the default -1 (unlimited axios maxBodyLength) + axios.post('http://localhost:4444/', { + data: data + }).then(function (res) { + assert.equal(res.data, 'OK', 'should handle response'); + done(); + }).catch(done); + }); + }); + it('should display error while parsing params', function (done) { server = http.createServer(function () { @@ -1287,9 +1367,11 @@ describe('supports http with nodejs', function () { }).catch(done); }); }); + describe('toFormData helper', function () { it('should properly serialize nested objects for parsing with multer.js (express.js)', function (done) { - const app = express(); + var app = express(); + var obj = { arr1: ['1', '2', '3'], arr2: ['1', ['2'], '3'], @@ -1324,4 +1406,130 @@ describe('supports http with nodejs', function () { }); }); }); + + describe('URLEncoded Form', function () { + it('should post object data as url-encoded form if content-type is application/x-www-form-urlencoded', function (done) { + var app = express(); + + var obj = { + arr1: ['1', '2', '3'], + arr2: ['1', ['2'], '3'], + obj: {x: '1', y: {z: '1'}}, + users: [{name: 'Peter', surname: 'griffin'}, {name: 'Thomas', surname: 'Anderson'}] + }; + + app.use(bodyParser.urlencoded({ extended: true })); + + app.post('/', function (req, res, next) { + res.send(JSON.stringify(req.body)); + }); + + server = app.listen(3001, function () { + return axios.post('http://localhost:3001/', obj, { + headers: { + 'content-type': 'application/x-www-form-urlencoded' + } + }) + .then(function (res) { + assert.deepStrictEqual(res.data, obj); + done(); + }).catch(done); + }); + }); + }); + + it('should respect formSerializer config', function (done) { + const obj = { + arr1: ['1', '2', '3'], + arr2: ['1', ['2'], '3'], + }; + + const form = new URLSearchParams(); + + form.append('arr1[0]', '1'); + form.append('arr1[1]', '2'); + form.append('arr1[2]', '3'); + + form.append('arr2[0]', '1'); + form.append('arr2[1][0]', '2'); + form.append('arr2[2]', '3'); + + server = http.createServer(function (req, res) { + req.pipe(res); + }).listen(3001, () => { + return axios.post('http://localhost:3001/', obj, { + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + formSerializer: { + indexes: true + } + }) + .then(function (res) { + assert.strictEqual(res.data, form.toString()); + done(); + }).catch(done); + }); + }); + + describe('Data URL', function () { + it('should support requesting data URL as a Buffer', function (done) { + const buffer = Buffer.from('123'); + + const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64'); + + axios.get(dataURI).then(({data})=> { + assert.deepStrictEqual(data, buffer); + done(); + }).catch(done); + }); + + it('should support requesting data URL as a Blob (if supported by the environment)', function (done) { + + if (!isBlobSupported) { + this.skip(); + return; + } + + const buffer = Buffer.from('123'); + + const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64'); + + axios.get(dataURI, {responseType: 'blob'}).then(async ({data})=> { + assert.strictEqual(data.type, 'application/octet-stream'); + assert.deepStrictEqual(await data.text(), '123'); + done(); + }).catch(done); + }); + + it('should support requesting data URL as a String (text)', function (done) { + const buffer = Buffer.from('123', 'utf-8'); + + const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64'); + + axios.get(dataURI, {responseType: "text"}).then(({data})=> { + assert.deepStrictEqual(data, '123'); + done(); + }).catch(done); + }); + + it('should support requesting data URL as a Stream', function (done) { + const buffer = Buffer.from('123', 'utf-8'); + + const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64'); + + axios.get(dataURI, {responseType: "stream"}).then(({data})=> { + var str = ''; + + data.on('data', function(response){ + str += response.toString(); + }); + + data.on('end', function(){ + assert.strictEqual(str, '123'); + done(); + }); + }).catch(done); + }); + }); }); diff --git a/test/unit/helpers/fromDataURI.js b/test/unit/helpers/fromDataURI.js new file mode 100644 index 0000000000..97ff84bf06 --- /dev/null +++ b/test/unit/helpers/fromDataURI.js @@ -0,0 +1,12 @@ +var assert = require('assert'); +var fromDataURI = require('../../../lib/helpers/fromDataURI'); + +describe('helpers::fromDataURI', function () { + it('should return buffer from data uri', function () { + const buffer= Buffer.from('123'); + + const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64'); + + assert.deepStrictEqual(fromDataURI(dataURI, false), buffer); + }); +});