Skip to content

Commit 7c93a8d

Browse files
authored
fix: multipart/form-data base64 parsing (nodejs#1668)
1 parent 1b85001 commit 7c93a8d

File tree

2 files changed

+37
-15
lines changed

2 files changed

+37
-15
lines changed

lib/fetch/body.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -434,16 +434,31 @@ function bodyMixinMethods (instance) {
434434
})
435435
busboy.on('file', (name, value, info) => {
436436
const { filename, encoding, mimeType } = info
437-
const base64 = encoding.toLowerCase() === 'base64'
438437
const chunks = []
439-
value.on('data', (chunk) => {
440-
if (base64) chunk = Buffer.from(chunk.toString(), 'base64')
441-
chunks.push(chunk)
442-
})
443-
value.on('end', () => {
444-
const file = new File(chunks, filename, { type: mimeType })
445-
responseFormData.append(name, file)
446-
})
438+
439+
if (encoding.toLowerCase() === 'base64') {
440+
let base64chunk = ''
441+
442+
value.on('data', (chunk) => {
443+
base64chunk += chunk.toString().replace(/[\r\n]/gm, '')
444+
445+
const end = base64chunk.length - base64chunk.length % 4
446+
chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))
447+
448+
base64chunk = base64chunk.slice(end)
449+
})
450+
value.on('end', () => {
451+
chunks.push(Buffer.from(base64chunk, 'base64'))
452+
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
453+
})
454+
} else {
455+
value.on('data', (chunk) => {
456+
chunks.push(chunk)
457+
})
458+
value.on('end', () => {
459+
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
460+
})
461+
}
447462
})
448463

449464
const busboyResolve = new Promise((resolve, reject) => {

test/fetch/client-fetch.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const nodeFetch = require('../../index-fetch')
1212
const { once } = require('events')
1313
const { gzipSync } = require('zlib')
1414
const { promisify } = require('util')
15+
const { randomFillSync, createHash } = require('crypto')
1516

1617
setGlobalDispatcher(new Agent({
1718
keepAliveTimeout: 1,
@@ -200,20 +201,26 @@ test('multipart formdata base64', (t) => {
200201
t.plan(1)
201202

202203
// Example form data with base64 encoding
203-
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--'
204-
const server = createServer((req, res) => {
204+
const data = randomFillSync(Buffer.alloc(256))
205+
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--`
206+
const server = createServer(async (req, res) => {
205207
res.setHeader('content-type', 'multipart/form-data; boundary=----formdata-undici-0.5786922755719377')
206-
res.write(formRaw)
208+
209+
for (let offset = 0; offset < formRaw.length;) {
210+
res.write(formRaw.slice(offset, offset += 2))
211+
await new Promise(resolve => setTimeout(resolve))
212+
}
207213
res.end()
208214
})
209215
t.teardown(server.close.bind(server))
210216

211217
server.listen(0, () => {
212218
fetch(`http://localhost:${server.address().port}`)
213219
.then(res => res.formData())
214-
.then(form => form.get('key').text())
215-
.then(text => {
216-
t.equal(text, 'value')
220+
.then(form => form.get('file').arrayBuffer())
221+
.then(buffer => createHash('sha256').update(Buffer.from(buffer)).digest('base64'))
222+
.then(digest => {
223+
t.equal(createHash('sha256').update(data).digest('base64'), digest)
217224
})
218225
})
219226
})

0 commit comments

Comments
 (0)