Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for multipart/form-data #1606

Merged
merged 10 commits into from
Sep 28, 2022
38 changes: 36 additions & 2 deletions lib/fetch/body.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const Busboy = require('busboy')
mcollina marked this conversation as resolved.
Show resolved Hide resolved
const util = require('../core/util')
const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util')
const { FormData } = require('./formdata')
Expand All @@ -8,9 +9,9 @@ const { webidl } = require('./webidl')
const { Blob } = require('buffer')
const { kBodyUsed } = require('../core/symbols')
const assert = require('assert')
const { NotSupportedError } = require('../core/errors')
const { isErrored } = require('../core/util')
const { isUint8Array, isArrayBuffer } = require('util/types')
const { File } = require('./file')

let ReadableStream

Expand Down Expand Up @@ -397,7 +398,40 @@ function bodyMixinMethods (instance) {

// If mimeType’s essence is "multipart/form-data", then:
if (/multipart\/form-data/.test(contentType)) {
throw new NotSupportedError('multipart/form-data not supported')
const headers = {}
for (const [key, value] of this.headers) headers[key.toLowerCase()] = value

const responseFormData = new FormData()

try {
const busboy = Busboy({ headers })
busboy.on('field', (name, value) => {
responseFormData.append(name, value)
})
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)
ronag marked this conversation as resolved.
Show resolved Hide resolved
})
value.on('end', () => {
const file = new File(chunks, filename, { type: mimeType })
responseFormData.append(name, file)
})
})
const busboyResolve = new Promise((resolve) => busboy.on('finish', resolve))

if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
busboy.end()
await busboyResolve
} catch (err) {
// busboy may fail to parse a malformed body, so throw a type error
throw Object.assign(new TypeError(), { cause: err })
}

return responseFormData
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:

Expand Down
35 changes: 26 additions & 9 deletions test/fetch/client-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,38 @@ test('unsupported formData 1', (t) => {
})
})

test('unsupported formData 2', (t) => {
t.plan(1)
test('multipart formdata', async (t) => {
t.plan(2)

// Construct example form data, with text and file fields
const formData = new FormData()
formData.append('field1', 'value1')
const blob = new Blob(['example\ntext file'], { type: 'text/plain' })
formData.append('field2', blob, 'file.txt')

const tempRes = new Response(formData)
const boundary = tempRes.headers.get('content-type').split('boundary=')[1]
const formRaw = await tempRes.text()

const server = createServer((req, res) => {
res.setHeader('content-type', 'multipart/form-data')
res.setHeader('content-type', 'multipart/form-data; boundary=' + boundary)
res.write(formRaw)
res.end()
})
t.teardown(server.close.bind(server))

server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.catch(err => {
t.equal(err.name, 'NotSupportedError')
})
await new Promise((resolve) => {
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}`)
.then(res => res.formData())
.then(formData => {
t.equal(formData.get('field1'), 'value1')
return formData.get('field2').text()
}).then(text => {
t.equal(text, 'example\ntext file')
resolve()
})
})
})
})

Expand Down