From 005937c72749dfa3914c8b6193a88c772a522275 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Sep 2022 15:28:32 +0300 Subject: [PATCH] feat(fetch): new `fieldsFirst` option to allow async stream consumption for multipart forms --- .changeset/brown-experts-look.md | 5 ++ packages/fetch/dist/getFormDataMethod.js | 91 +++++++++++++++++++----- packages/fetch/dist/index.d.ts | 2 + 3 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 .changeset/brown-experts-look.md diff --git a/.changeset/brown-experts-look.md b/.changeset/brown-experts-look.md new file mode 100644 index 0000000000..805ae6123e --- /dev/null +++ b/.changeset/brown-experts-look.md @@ -0,0 +1,5 @@ +--- +'@whatwg-node/fetch': minor +--- + +feat(fetch): new `fieldsFirst` option to allow async stream consumption for multipart forms diff --git a/packages/fetch/dist/getFormDataMethod.js b/packages/fetch/dist/getFormDataMethod.js index bed5e4a237..2530601662 100644 --- a/packages/fetch/dist/getFormDataMethod.js +++ b/packages/fetch/dist/getFormDataMethod.js @@ -1,7 +1,35 @@ const busboy = require('busboy'); +const { resolve } = require('path'); const streams = require("stream"); module.exports = function getFormDataMethod(File, limits) { + function consumeStreamAsFile({ + name, + filename, + mimeType, + fileStream, + formData, + }) { + if (fileStream._consumedAsFile) { + return Promise.resolve(formData.get(name)); + } + return new Promise((resolve, reject) => { + const chunks = []; + fileStream.on('limit', () => { + reject(new Error(`File size limit exceeded: ${limits.fileSize} bytes`)); + }) + fileStream.on('data', (chunk) => { + chunks.push(chunk); + }) + fileStream.on('close', () => { + const file = new File(chunks, filename, { type: mimeType }); + formData.set(name, file); + fileStream._consumedAsFile = true; + resolve(file); + }); + }) + } + return function formData() { if (this.body == null) { return null; @@ -17,18 +45,6 @@ module.exports = function getFormDataMethod(File, limits) { }); return new Promise((resolve, reject) => { const formData = new Map(); - bb.on('file', (name, fileStream, { filename, mimeType }) => { - const chunks = []; - fileStream.on('limit', () => { - reject(new Error(`File size limit exceeded: ${limits.fileSize} bytes`)); - }) - fileStream.on('data', (chunk) => { - chunks.push(chunk); - }) - fileStream.on('close', () => { - formData.set(name, new File(chunks, filename, { type: mimeType })); - }); - }) bb.on('field', (name, value, { nameTruncated, valueTruncated }) => { if (nameTruncated) { reject(new Error(`Field name size exceeded: ${limits.fieldNameSize} bytes`)); @@ -38,14 +54,57 @@ module.exports = function getFormDataMethod(File, limits) { } formData.set(name, value) }) - bb.on('partsLimit', () => { - reject(new Error(`Parts limit exceeded: ${limits.parts}`)); + bb.on('fieldsLimit', () => { + reject(new Error(`Fields limit exceeded: ${limits.fields}`)); + }) + bb.on('file', (name, fileStream, { filename, mimeType }) => { + if (limits.fieldsFirst) { + resolve(formData); + const fakeFileObj = { + name: filename, + type: mimeType, + } + Object.setPrototypeOf(fakeFileObj, File.prototype); + formData.set(name, new Proxy(fakeFileObj, { + get: (target, prop) => { + switch(prop) { + case 'name': + return filename; + case 'type': + return mimeType; + case 'stream': + return () => fileStream; + case 'size': + throw new Error(`Cannot access file size before consuming the stream.`); + case 'slice': + throw new Error(`Cannot slice file before consuming the stream.`); + case 'text': + case 'arrayBuffer': + return () => consumeStreamAsFile({ + name, + filename, + mimeType, + fileStream, + formData, + }).then(file => file[prop]()) + } + }, + })) + } else { + consumeStreamAsFile({ + name, + filename, + mimeType, + fileStream, + formData, + }).catch(err => reject(err)); + } }) bb.on('filesLimit', () => { reject(new Error(`Files limit exceeded: ${limits.files}`)); }) - bb.on('fieldsLimit', () => { - reject(new Error(`Fields limit exceeded: ${limits.fields}`)); + bb.on('partsLimit', () => { + reject(new Error(`Parts limit exceeded: ${limits.parts}`)); }) bb.on('close', () => { resolve(formData); diff --git a/packages/fetch/dist/index.d.ts b/packages/fetch/dist/index.d.ts index 03204c2203..a1df4f89ee 100644 --- a/packages/fetch/dist/index.d.ts +++ b/packages/fetch/dist/index.d.ts @@ -51,6 +51,8 @@ declare module "@whatwg-node/fetch" { parts?: number; /* For multipart forms, the max number of header key-value pairs to parse. Default: 2000. */ headerSize?: number; + /* For multipart forms, enable this if your data has fields first, then files. Default: false. */ + fieldsFirst?: boolean; } export const createFetch: (opts?: { useNodeFetch?: boolean; formDataLimits?: FormDataLimits }) => ({ fetch: typeof _fetch,