diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 3cb561c4478..b6dadce055c 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -434,16 +434,31 @@ function bodyMixinMethods (instance) { }) busboy.on('file', (name, value, info) => { const { filename, encoding, mimeType } = info - const base64 = encoding.toLowerCase() === 'base64' const chunks = [] - value.on('data', (chunk) => { - if (base64) chunk = Buffer.from(chunk.toString(), 'base64') - chunks.push(chunk) - }) - value.on('end', () => { - const file = new File(chunks, filename, { type: mimeType }) - responseFormData.append(name, file) - }) + + if (encoding.toLowerCase() === 'base64') { + let base64chunk = '' + + value.on('data', (chunk) => { + base64chunk += chunk.toString().replace(/[\r\n]/gm, '') + + const end = base64chunk.length - base64chunk.length % 4 + chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) + + base64chunk = base64chunk.slice(end) + }) + value.on('end', () => { + chunks.push(Buffer.from(base64chunk, 'base64')) + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } else { + value.on('data', (chunk) => { + chunks.push(chunk) + }) + value.on('end', () => { + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } }) const busboyResolve = new Promise((resolve, reject) => { diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js index 5310f39a4c2..685e196276a 100644 --- a/test/fetch/client-fetch.js +++ b/test/fetch/client-fetch.js @@ -12,6 +12,7 @@ const nodeFetch = require('../../index-fetch') const { once } = require('events') const { gzipSync } = require('zlib') const { promisify } = require('util') +const { randomFillSync, createHash } = require('crypto') setGlobalDispatcher(new Agent({ keepAliveTimeout: 1, @@ -200,10 +201,15 @@ test('multipart formdata base64', (t) => { t.plan(1) // Example form data with base64 encoding - const formRaw = '------formdata-undici-0.5786922755719377\r\nContent-Disposition: form-data; name="key"; filename="test.txt"\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\ndmFsdWU=\r\n------formdata-undici-0.5786922755719377--' - const server = createServer((req, res) => { + const data = randomFillSync(Buffer.alloc(256)) + const formRaw = `------formdata-undici-0.5786922755719377\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: base64\r\n\r\n${data.toString('base64')}\r\n------formdata-undici-0.5786922755719377--` + const server = createServer(async (req, res) => { res.setHeader('content-type', 'multipart/form-data; boundary=----formdata-undici-0.5786922755719377') - res.write(formRaw) + + for (let offset = 0; offset < formRaw.length;) { + res.write(formRaw.slice(offset, offset += 2)) + await new Promise(resolve => setTimeout(resolve)) + } res.end() }) t.teardown(server.close.bind(server)) @@ -211,9 +217,10 @@ test('multipart formdata base64', (t) => { server.listen(0, () => { fetch(`http://localhost:${server.address().port}`) .then(res => res.formData()) - .then(form => form.get('key').text()) - .then(text => { - t.equal(text, 'value') + .then(form => form.get('file').arrayBuffer()) + .then(buffer => createHash('sha256').update(Buffer.from(buffer)).digest('base64')) + .then(digest => { + t.equal(createHash('sha256').update(data).digest('base64'), digest) }) }) })