From bf78297f4d3340deea5d5e9bce2894d3572a938a Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Tue, 22 Nov 2022 19:33:34 +0200 Subject: [PATCH 1/3] Draft; --- .eslintrc.cjs | 4 +- lib/adapters/http.js | 19 +++++- lib/helpers/formDataToStream.js | 102 ++++++++++++++++++++++++++++++ lib/helpers/toFormData.js | 13 +--- lib/utils.js | 36 ++++++++++- package-lock.json | 64 +++++++++++++++++++ package.json | 3 +- test/unit/adapters/http.js | 108 ++++++++++++++++++++++---------- 8 files changed, 299 insertions(+), 50 deletions(-) create mode 100644 lib/helpers/formDataToStream.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a6a4cc6614..11c8ad99db 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,12 +1,12 @@ module.exports = { "env": { "browser": true, - "es2017": true, + "es2018": true, "node": true }, "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 2017, + "ecmaVersion": 2018, "sourceType": "module" }, "rules": { diff --git a/lib/adapters/http.js b/lib/adapters/http.js index b561645d2f..c622106e96 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -19,6 +19,7 @@ import stream from 'stream'; import AxiosHeaders from '../core/AxiosHeaders.js'; import AxiosTransformStream from '../helpers/AxiosTransformStream.js'; import EventEmitter from 'events'; +import formDataToStream from "../helpers/formDataToStream.js"; const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress); @@ -230,9 +231,23 @@ export default function httpAdapter(config) { let maxUploadRate = undefined; let maxDownloadRate = undefined; - // support for https://www.npmjs.com/package/form-data api - if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { + // support for spec compliant FormData objects + if (utils.isSpecCompliantForm(data)) { + const userBoundary = headers.getContentType(/boundary=([-_\w\d]{10,70})/i); + + data = formDataToStream(data, (formHeaders) => { + headers.set(formHeaders); + }, { + tag: `axios-${VERSION}-boundary`, + boundary: userBoundary && userBoundary[1] || undefined + }); + // support for https://www.npmjs.com/package/form-data api + } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { headers.set(data.getHeaders()); + if (utils.isFunction(data.getLengthSync)) { // check if the undocumented API exists + const knownLength = data.getLengthSync(); + !utils.isUndefined(knownLength) && headers.setContentLength(knownLength, false); + } } else if (data && !utils.isStream(data)) { if (Buffer.isBuffer(data)) { // Nothing to do... diff --git a/lib/helpers/formDataToStream.js b/lib/helpers/formDataToStream.js new file mode 100644 index 0000000000..4b092336a5 --- /dev/null +++ b/lib/helpers/formDataToStream.js @@ -0,0 +1,102 @@ +import {Readable} from 'stream'; +import utils from "../utils.js"; + +const BOUNDARY_ALPHABET = utils.ALPHABET.ALPHA_DIGIT + '-_'; + +const textEncoder = new TextEncoder(); + +const CRLF = '\r\n'; +const CRLF_BYTES = textEncoder.encode(CRLF); +const CRLF_BYTES_COUNT = 2; + +class FormDataPart { + constructor(name, value) { + const {escapeName} = this.constructor; + const type = this.type = utils.kindOf(value); + const isFile = type === 'file'; + + this.name = name; + + this.value = value; + + if (type !== 'blob' && !isFile) { + value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF)); + } else { + this.isBlob = true; + } + + const filename = isFile ? `; filename="${escapeName(value.name)}"` : ''; + + let headers = `Content-Disposition: form-data; name="${escapeName(name)}"${filename}${CRLF}`; + + if (isFile) { + headers += `Content-Type: ${value.type || "application/octet-stream"}${CRLF}` + } + + this.headers = textEncoder.encode(headers + CRLF); + + this.contentLength = this.isBlob ? value.size : value.byteLength; + + this.size = this.headers.byteLength + this.contentLength + CRLF_BYTES_COUNT; + } + + *encode(){ + yield this.headers; + + yield this.value; + + yield CRLF_BYTES; + } + + static escapeName(name) { + return String(name).replace(/[\r\n"]/g, (match) => ({ + '\r' : '%0D', + '\n' : '%0A', + '"' : '%22', + }[match])); + } +} + +const formDataToStream = (form, headersHandler, options) => { + const { + tag = 'form-data-boundary', + size = 25, + boundary = tag + '-' + utils.generateString(size, BOUNDARY_ALPHABET) + } = options || {}; + + if(!utils.isFormData(form)) { + throw TypeError('FormData instance required'); + } + + if (boundary.length < 1 || boundary.length > 70) { + throw Error('boundary must be 10-70 characters long') + } + + const boundaryBytes = textEncoder.encode('--' + boundary + CRLF); + const footerBytes = textEncoder.encode('--' + boundary + '--' + CRLF + CRLF); + let contentLength = footerBytes.byteLength; + + const parts = Array.from(form.entries()).map(([name, value]) => { + const part = new FormDataPart(name, value); + contentLength += part.size; + return part; + }); + + contentLength += boundaryBytes.byteLength * parts.length; + + headersHandler && headersHandler({ + 'Content-Length': contentLength, + 'Content-Type': `multipart/form-data; boundary=${boundary}` + }); + + return Readable.from((async function *() { + for(const part of parts) { + yield boundaryBytes; + yield* part.encode(); + } + + yield footerBytes; + })()); +}; + +export default formDataToStream; diff --git a/lib/helpers/toFormData.js b/lib/helpers/toFormData.js index ce99ce15c7..107ec051d3 100644 --- a/lib/helpers/toFormData.js +++ b/lib/helpers/toFormData.js @@ -59,17 +59,6 @@ const predicates = utils.toFlatObject(utils, {}, null, function filter(prop) { return /^is[A-Z]/.test(prop); }); -/** - * If the thing is a FormData object, return true, otherwise return false. - * - * @param {unknown} thing - The thing to check. - * - * @returns {boolean} - */ -function isSpecCompliant(thing) { - return thing && utils.isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]; -} - /** * Convert a data object to FormData * @@ -117,7 +106,7 @@ function toFormData(obj, formData, options) { const dots = options.dots; const indexes = options.indexes; const _Blob = options.Blob || typeof Blob !== 'undefined' && Blob; - const useBlob = _Blob && isSpecCompliant(formData); + const useBlob = _Blob && utils.isSpecCompliantForm(formData); if (!utils.isFunction(visitor)) { throw new TypeError('visitor must be a function'); diff --git a/lib/utils.js b/lib/utils.js index 849d0444b1..5a9a1ee8cb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -592,6 +592,37 @@ const toFiniteNumber = (value, defaultValue) => { return Number.isFinite(value) ? value : defaultValue; } +const ALPHA = 'abcdefghijklmnopqrstuvwxyz' + +const DIGIT = '0123456789'; + +const ALPHABET = { + DIGIT, + ALPHA, + ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT +} + +const generateString = (size = 16, alphabet = ALPHABET.ALPHA_DIGIT) => { + let str = ''; + const {length} = alphabet; + while (size--) { + str += alphabet[Math.random() * length|0] + } + + return str; +} + +/** + * If the thing is a FormData object, return true, otherwise return false. + * + * @param {unknown} thing - The thing to check. + * + * @returns {boolean} + */ +function isSpecCompliantForm(thing) { + return thing && isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]; +} + export default { isArray, isArrayBuffer, @@ -637,5 +668,8 @@ export default { toFiniteNumber, findKey, global: _global, - isContextDefined + isContextDefined, + ALPHABET, + generateString, + isSpecCompliantForm }; diff --git a/package-lock.json b/package-lock.json index 3bce419237..784c1e9986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "es6-promise": "^4.2.8", "eslint": "^8.17.0", "express": "^4.18.1", + "formdata-node": "^5.0.0", "formidable": "^2.0.1", "fs-extra": "^10.1.0", "get-stream": "^3.0.0", @@ -7936,6 +7937,19 @@ "node": ">= 6" } }, + "node_modules/formdata-node": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-5.0.0.tgz", + "integrity": "sha512-zrGsVVS56jJo+htsVv7ffXuzie91a2NrU1cPamvtPaSyRX++SH+4KXlGoOt+ncgDJ4bFA2SAQ+QGA+p4l1vciw==", + "dev": true, + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 14.17" + } + }, "node_modules/formidable": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", @@ -12108,6 +12122,25 @@ "tslib": "^2.0.3" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -17247,6 +17280,15 @@ "node": ">=0.10.0" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/webdriver": { "version": "6.12.1", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-6.12.1.tgz", @@ -24497,6 +24539,16 @@ "mime-types": "^2.1.12" } }, + "formdata-node": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-5.0.0.tgz", + "integrity": "sha512-zrGsVVS56jJo+htsVv7ffXuzie91a2NrU1cPamvtPaSyRX++SH+4KXlGoOt+ncgDJ4bFA2SAQ+QGA+p4l1vciw==", + "dev": true, + "requires": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + } + }, "formidable": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", @@ -27917,6 +27969,12 @@ "tslib": "^2.0.3" } }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -32129,6 +32187,12 @@ } } }, + "web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true + }, "webdriver": { "version": "6.12.1", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-6.12.1.tgz", diff --git a/package.json b/package.json index e3c56ab7a9..26da95635a 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "es6-promise": "^4.2.8", "eslint": "^8.17.0", "express": "^4.18.1", + "formdata-node": "^5.0.0", "formidable": "^2.0.1", "fs-extra": "^10.1.0", "get-stream": "^3.0.0", @@ -138,4 +139,4 @@ "Daniel Lopretto (https://github.com/timemachine3030)" ], "sideEffects": false -} \ No newline at end of file +} diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index fba276d141..e53f5d3ee1 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -11,7 +11,7 @@ import fs from 'fs'; import path from 'path'; let server, proxy; import AxiosError from '../../../lib/core/AxiosError.js'; -import FormData from 'form-data'; +import FormDataLegacy from 'form-data'; import formidable from 'formidable'; import express from 'express'; import multer from 'multer'; @@ -21,6 +21,11 @@ import {Throttle} from 'stream-throttle'; import devNull from 'dev-null'; import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill.js'; import {__setProxy} from "../../../lib/adapters/http.js"; +import {FormData as FormDataPolyfill, Blob as BlobPolyfill, File as FilePolyfill} from 'formdata-node'; + +const FormDataSpecCompliant = typeof FormData !== 'undefined' ? FormData : FormDataPolyfill; +const BlobSpecCompliant = typeof Blob !== 'undefined' ? Blob : BlobPolyfill; +const FileSpecCompliant = typeof File !== 'undefined' ? File : FilePolyfill; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -80,6 +85,12 @@ function startHTTPServer({useBuffering= false, rate = undefined, port = 4444} = }); } +const handleFormData = (req) => { + const form = new formidable.IncomingForm(); + + return util.promisify(form.parse).call(form, req); +} + function generateReadableStream(length = 1024 * 1024, chunkSize = 10 * 1024, sleep = 50) { return stream.Readable.from(async function* (){ let dataLength = 0; @@ -1468,44 +1479,77 @@ describe('supports http with nodejs', function () { }); describe('FormData', function () { - it('should allow passing FormData', function (done) { - var form = new FormData(); - var file1 = Buffer.from('foo', 'utf8'); - - form.append('foo', "bar"); - form.append('file1', file1, { - filename: 'bar.jpg', - filepath: 'temp/bar.jpg', - contentType: 'image/jpeg' + describe('form-data instance (https://www.npmjs.com/package/form-data)', (done) => { + it('should allow passing FormData', function (done) { + var form = new FormDataLegacy(); + var file1 = Buffer.from('foo', 'utf8'); + + form.append('foo', "bar"); + form.append('file1', file1, { + filename: 'bar.jpg', + filepath: 'temp/bar.jpg', + contentType: 'image/jpeg' + }); + + server = http.createServer(function (req, res) { + var receivedForm = new formidable.IncomingForm(); + + receivedForm.parse(req, function (err, fields, files) { + if (err) { + return done(err); + } + + res.end(JSON.stringify({ + fields: fields, + files: files + })); + }); + }).listen(4444, function () { + axios.post('http://localhost:4444/', form, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }).then(function (res) { + assert.deepStrictEqual(res.data.fields, {foo: 'bar'}); + + assert.strictEqual(res.data.files.file1.mimetype, 'image/jpeg'); + assert.strictEqual(res.data.files.file1.originalFilename, 'temp/bar.jpg'); + assert.strictEqual(res.data.files.file1.size, 3); + + done(); + }).catch(done); + }); }); + }); - server = http.createServer(function (req, res) { - var receivedForm = new formidable.IncomingForm(); + describe('SpecCompliant FormData', (done) => { + it('should allow passing FormData', async () => { + server = await startHTTPServer((req, res) => { + var receivedForm = new formidable.IncomingForm(); - receivedForm.parse(req, function (err, fields, files) { - if (err) { - return done(err); - } + receivedForm.parse(req, function (err, fields, files) { + if (err) { + return done(err); + } - res.end(JSON.stringify({ - fields: fields, - files: files - })); + res.end(JSON.stringify({ + fields: fields, + files: files + })); + }); }); - }).listen(4444, function () { - axios.post('http://localhost:4444/', form, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }).then(function (res) { - assert.deepStrictEqual(res.data.fields, {foo: 'bar'}); - assert.strictEqual(res.data.files.file1.mimetype, 'image/jpeg'); - assert.strictEqual(res.data.files.file1.originalFilename, 'temp/bar.jpg'); - assert.strictEqual(res.data.files.file1.size, 3); + const form = new FormDataSpecCompliant(); - done(); - }).catch(done); + const blob = new BlobSpecCompliant(['test'], {type: 'image/jpeg'}) + + form.append('foo', 'bar'); + + //form.append('file1', blob); + + await axios.post(LOCAL_SERVER_URL, form).then(function (res) { + assert.deepStrictEqual(res.data.fields, {foo: 'bar'}); + }); }); }); From c69c1ad34c54b9a22becf44ae9b0c34ba06874b3 Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Tue, 31 Jan 2023 01:01:27 +0200 Subject: [PATCH 2/3] feat(helper): improved `readBlob` helper; --- lib/helpers/readBlob.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/helpers/readBlob.js b/lib/helpers/readBlob.js index e5bb4cb24d..19f147681e 100644 --- a/lib/helpers/readBlob.js +++ b/lib/helpers/readBlob.js @@ -1,8 +1,14 @@ -const readBlob = async function *(blob) { +const {asyncIterator} = Symbol; + +const readBlob = async function* (blob) { if (blob.stream) { yield* blob.stream() - } else { + } else if (blob.arrayBuffer) { yield await blob.arrayBuffer() + } else if (blob[asyncIterator]) { + yield* asyncIterator.call(blob); + } else { + yield blob; } } From a882a0ed8305067f161bee9bab4409438d17a858 Mon Sep 17 00:00:00 2001 From: DigitalBrainJS Date: Thu, 2 Feb 2023 00:25:37 +0200 Subject: [PATCH 3/3] fix(formdata): add hotfix to use the asynchronous API to compute the content-length header value; --- lib/adapters/http.js | 17 ++++++++++++----- lib/core/AxiosHeaders.js | 2 +- test/unit/adapters/axios.png | Bin 0 -> 1716 bytes test/unit/adapters/http.js | 12 ++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 test/unit/adapters/axios.png diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 17e3a1de9d..d03db3a622 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -7,6 +7,7 @@ import buildURL from './../helpers/buildURL.js'; import {getProxyForUrl} from 'proxy-from-env'; import http from 'http'; import https from 'https'; +import util from 'util'; import followRedirects from 'follow-redirects'; import zlib from 'zlib'; import {VERSION} from '../env/data.js'; @@ -117,7 +118,8 @@ const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(pr /*eslint consistent-return:0*/ export default isHttpAdapterSupported && function httpAdapter(config) { - return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { + /*eslint no-async-promise-executor:0*/ + return new Promise(async function dispatchHttpRequest(resolvePromise, rejectPromise) { let data = config.data; const responseType = config.responseType; const responseEncoding = config.responseEncoding; @@ -208,7 +210,7 @@ export default isHttpAdapterSupported && function httpAdapter(config) { convertedData = convertedData.toString(responseEncoding); if (!responseEncoding || responseEncoding === 'utf8') { - data = utils.stripBOM(convertedData); + convertedData = utils.stripBOM(convertedData); } } else if (responseType === 'stream') { convertedData = stream.Readable.from(convertedData); @@ -258,9 +260,14 @@ export default isHttpAdapterSupported && function httpAdapter(config) { // support for https://www.npmjs.com/package/form-data api } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { headers.set(data.getHeaders()); - if (utils.isFunction(data.getLengthSync)) { // check if the undocumented API exists - const knownLength = data.getLengthSync(); - !utils.isUndefined(knownLength) && headers.setContentLength(knownLength, false); + + if (!headers.hasContentLength()) { + try { + const knownLength = await util.promisify(data.getLength).call(data); + headers.setContentLength(knownLength); + /*eslint no-empty:0*/ + } catch (e) { + } } } else if (utils.isBlob(data)) { data.size && headers.setContentType(data.type || 'application/octet-stream'); diff --git a/lib/core/AxiosHeaders.js b/lib/core/AxiosHeaders.js index 858a3d078c..1cf84b9458 100644 --- a/lib/core/AxiosHeaders.js +++ b/lib/core/AxiosHeaders.js @@ -141,7 +141,7 @@ class AxiosHeaders { if (header) { const key = utils.findKey(this, header); - return !!(key && (!matcher || matchHeaderValue(this, this[key], key, matcher))); + return !!(key && this[key] !== undefined && (!matcher || matchHeaderValue(this, this[key], key, matcher))); } return false; diff --git a/test/unit/adapters/axios.png b/test/unit/adapters/axios.png new file mode 100644 index 0000000000000000000000000000000000000000..8511175953c2ef062ebb6d073df7715a4037a7f6 GIT binary patch literal 1716 zcmah~>08o?8pUPYQWI2~NG&GZmrRXter~w0hzld6xs*aST8Ilw)5I0G%$o~2gq9Aj zh?bO^x!gOZb*EH79nCR!YYlwPQbNjGUQ{3k;beZYCD5Qq^xPzKS25=`5G3sj z6lRG>X&{h_pPwf>oEE)$yH{v^N;4-h&!vH(W&D5?aKOq;aYQKq#$lK`t0NFlU#sG4 zF8v`6f4oOv&czT*YHGM!!9=O9WWs)}peQSg`?^Cu--(hr(QFEVgOvquL|Kx*i|{Fu zVAt(*@d?+xvrvlL)t|w;yKm=Oq)#vR?ACnaX2TL=5jn|d@WqsC+p^#nzmVHe507o( zAb*~2gbvuPVkq<-G1szZXF2tvKg91b3!82GO5oTihF#A^n{Ipv&~)g}M$ zI=N2opw}&oJ;1mzRW6I<&CW2bi!iTpor4LIfu`V>f-C`>3~?N)Fj9^Bge=1F7?`+J zS?#wLH;Z%4sI{4T7Ut{H;j6Z9#g@Idrt=pY={NA1S(YJ?*zYDhvNt4?cLBhf*+tKE zRNVR@sMjcX3oZ}57O<^JrQE>nDatjwl7iysfq@;jR(Zn_M0%2fa+}KP*o*=4;rU8E zwT6?%UNrM>J$i%Fa(hl9ez~%YGbWL!~VHXG$hwYt#d- zF;#~1L%S#0W=Lk{_1f<6eKu5Rny`rhYlK%WAZ(2(n8p-06eb_HWU2`VF^1i(>HoUY z*=CJN7(C-5PzdAI=6bZmcZ_a(5&+_Q1M0!O)n2_sim+aZx~aUO#Dp-Xz?a6$0p?iq`v_>j?oI2lm-2_9TM&&C`k( ztP2t2o?%#Yc7nCmk|gbOtH(rQXC$@!W!dX9<~z3BfWd{8^QvD?InI;2wI}0{I#oQZE^Tg%ai2EibVx}3FI~> zF}a=w%+C?AnG*NewY${=_#^BU}zQt*t zQSR$-LIXvI!o3MXl)t#w|Gh1q_pT8uN9VtaURnXCn zPciy;3V>I^gDpsz+Ww5iScNF}g$WlkuX_CI#{m&UhnaoJTGUFUxl*+O&JUmB2~7sj*MJ(lr* D`%5b= literal 0 HcmV?d00001 diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 23faa3b92d..5b500f957e 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -1576,6 +1576,10 @@ describe('supports http with nodejs', function () { it('should allow passing FormData', function (done) { var form = new FormDataLegacy(); var file1 = Buffer.from('foo', 'utf8'); + const image = path.resolve(__dirname, './axios.png'); + const fileStream = fs.createReadStream(image); + + const stat = fs.statSync(image); form.append('foo', "bar"); form.append('file1', file1, { @@ -1584,9 +1588,13 @@ describe('supports http with nodejs', function () { contentType: 'image/jpeg' }); + form.append('fileStream', fileStream); + server = http.createServer(function (req, res) { var receivedForm = new formidable.IncomingForm(); + assert.ok(req.rawHeaders.find(header => header.toLowerCase() === 'content-length')); + receivedForm.parse(req, function (err, fields, files) { if (err) { return done(err); @@ -1609,6 +1617,10 @@ describe('supports http with nodejs', function () { assert.strictEqual(res.data.files.file1.originalFilename, 'temp/bar.jpg'); assert.strictEqual(res.data.files.file1.size, 3); + assert.strictEqual(res.data.files.fileStream.mimetype, 'image/png'); + assert.strictEqual(res.data.files.fileStream.originalFilename, 'axios.png'); + assert.strictEqual(res.data.files.fileStream.size, stat.size); + done(); }).catch(done); });