diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js index 37070b7d089..966fb0963f5 100644 --- a/lib/fetch/formdata.js +++ b/lib/fetch/formdata.js @@ -1,9 +1,8 @@ 'use strict' -const { Blob } = require('buffer') +const { isBlobLike, toUSVString } = require('./util') const { kState } = require('./symbols') const { File } = require('./file') -const { toUSVString } = require('./util') class FormData { constructor (...args) { @@ -25,7 +24,7 @@ class FormData { `Failed to execute 'append' on 'FormData': 2 arguments required, but only ${args.length} present.` ) } - if (args.length === 3 && !(args[1] instanceof Blob)) { + if (args.length === 3 && !isBlobLike(args[1])) { throw new TypeError( "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" ) @@ -34,7 +33,7 @@ class FormData { const filename = args.length === 3 ? toUSVString(args[2]) : undefined // 1. Let value be value if given; otherwise blobValue. - const value = args[1] instanceof Blob ? args[1] : toUSVString(args[1]) + const value = isBlobLike(args[1]) ? args[1] : toUSVString(args[1]) // 2. Let entry be the result of creating an entry with // name, value, and filename if given. @@ -135,7 +134,7 @@ class FormData { `Failed to execute 'set' on 'FormData': 2 arguments required, but only ${args.length} present.` ) } - if (args.length === 3 && !(args[1] instanceof Blob)) { + if (args.length === 3 && !isBlobLike(args[1])) { throw new TypeError( "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" ) @@ -147,7 +146,7 @@ class FormData { // are: // 1. Let value be value if given; otherwise blobValue. - const value = args[1] instanceof Blob ? args[1] : toUSVString(args[1]) + const value = isBlobLike(args[1]) ? args[1] : toUSVString(args[1]) // 2. Let entry be the result of creating an entry with name, value, and // filename if given. @@ -214,7 +213,7 @@ function makeEntry (name, value, filename) { // 3. If value is a Blob object and not a File object, then set value to a new File // object, representing the same bytes, whose name attribute value is "blob". - if (value instanceof Blob && !(value instanceof File)) { + if (isBlobLike(value) && !(value instanceof File)) { value = new File([value], 'blob') } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 61ae8d5f115..e3ba281d2b1 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -2,6 +2,7 @@ const { redirectStatus } = require('./constants') const { performance } = require('perf_hooks') +const { Blob } = require('buffer') const nodeUtil = require('util') let ReadableStream @@ -68,6 +69,18 @@ function requestBadPort (request) { return 'allowed' } +// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +function isBlobLike (object) { + return object instanceof Blob || ( + object && + typeof object === 'object' && + typeof object.constructor === 'function' && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ) +} + // Check whether |statusText| is a ByteString and // matches the Reason-Phrase token production. // RFC 2616: https://tools.ietf.org/html/rfc2616 @@ -349,5 +362,6 @@ module.exports = { requestCurrentURL, responseURL, responseLocationURL, + isBlobLike, isValidReasonPhrase } diff --git a/package.json b/package.json index 1c39886bee2..411121baed4 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "cronometro": "^0.8.0", "delay": "^5.0.0", "docsify-cli": "^4.4.3", + "formdata-node": "^4.3.1", "https-pem": "^2.0.0", "husky": "^7.0.2", "jest": "^27.2.0", diff --git a/test/fetch/formdata.js b/test/fetch/formdata.js index 02ba5b8c4ef..38ab1896153 100644 --- a/test/fetch/formdata.js +++ b/test/fetch/formdata.js @@ -2,6 +2,7 @@ const { test } = require('tap') const { FormData, File } = require('../../') +const { Blob: ThirdPartyBlob } = require('formdata-node') const { Blob } = require('buffer') test('arg validation', (t) => { @@ -104,6 +105,18 @@ test('append blob', async (t) => { t.end() }) +test('append third-party blob', async (t) => { + const form = new FormData() + form.set('asd', new ThirdPartyBlob(['asd1'])) + + t.equal(form.has('asd'), true) + t.equal(await form.get('asd').text(), 'asd1') + form.delete('asd') + t.equal(form.get('asd'), null) + + t.end() +}) + test('append string', (t) => { const form = new FormData() form.set('k1', 'v1')