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

implement sync formdata parser #2911

Merged
merged 11 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
184 changes: 47 additions & 137 deletions lib/web/fetch/body.js
@@ -1,6 +1,5 @@
'use strict'

const Busboy = require('@fastify/busboy')
const util = require('../../core/util')
const {
ReadableStreamFrom,
Expand All @@ -9,23 +8,20 @@ const {
readableStreamClose,
createDeferredPromise,
fullyReadBody,
extractMimeType
extractMimeType,
utf8DecodeBytes
} = require('./util')
const { FormData } = require('./formdata')
const { kState } = require('./symbols')
const { webidl } = require('./webidl')
const { Blob, File: NativeFile } = require('node:buffer')
const { Blob } = require('node:buffer')
const assert = require('node:assert')
const { isErrored } = require('../../core/util')
const { isArrayBuffer } = require('node:util/types')
const { File: UndiciFile } = require('./file')
const { serializeAMimeType } = require('./data-url')
const { Readable } = require('node:stream')
const { multipartFormDataParser } = require('./formdata-parser')

/** @type {globalThis['File']} */
const File = NativeFile ?? UndiciFile
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()

// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) {
Expand Down Expand Up @@ -338,116 +334,56 @@ function bodyMixinMethods (instance) {
return consumeBody(this, parseJSONFromBytes, instance)
},

async formData () {
webidl.brandCheck(this, instance)

throwIfAborted(this[kState])

// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(this)

// If mimeType’s essence is "multipart/form-data", then:
if (mimeType !== null && mimeType.essence === 'multipart/form-data') {
const responseFormData = new FormData()

let busboy

try {
busboy = new Busboy({
headers: {
'content-type': serializeAMimeType(mimeType)
},
preservePath: true
})
} catch (err) {
throw new DOMException(`${err}`, 'AbortError')
}

busboy.on('field', (name, value) => {
responseFormData.append(name, value)
})
busboy.on('file', (name, value, filename, encoding, mimeType) => {
const chunks = []

if (encoding === 'base64' || 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) => {
busboy.on('finish', resolve)
busboy.on('error', (err) => reject(new TypeError(err)))
})

if (this.body !== null) {
Readable.from(this[kState].body.stream).pipe(busboy)
}
formData () {
// The formData() method steps are to return the result of running
// consume body with this and the following step given a byte sequence bytes:
return consumeBody(this, (value) => {
// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(this)

// 2. If mimeType is non-null, then switch on mimeType’s essence and run
// the corresponding steps:
if (mimeType !== null) {
switch (mimeType.essence) {
case 'multipart/form-data': {
// 1. ... [long step]
const parsed = multipartFormDataParser(value, mimeType)

// 2. If that fails for some reason, then throw a TypeError.
if (parsed === 'failure') {
throw new TypeError('Failed to parse body as FormData.')
}

// 3. Return a new FormData object, appending each entry,
// resulting from the parsing operation, to its entry list.
const fd = new FormData()
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
fd[kState] = parsed

return fd
}
case 'application/x-www-form-urlencoded': {
// 1. Let entries be the result of parsing bytes.
const entries = new URLSearchParams(value.toString())
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved

await busboyResolve
// 2. If entries is failure, then throw a TypeError.
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved

return responseFormData
} else if (mimeType !== null && mimeType.essence === 'application/x-www-form-urlencoded') {
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
// 3. Return a new FormData object whose entry list is entries.
const fd = new FormData()

// 1. Let entries be the result of parsing bytes.
let entries
try {
let text = ''
// application/x-www-form-urlencoded parser will keep the BOM.
// https://url.spec.whatwg.org/#concept-urlencoded-parser
// Note that streaming decoder is stateful and cannot be reused
const stream = this[kState].body.stream.pipeThrough(new TextDecoderStream('utf-8', { ignoreBOM: true }))
for (const [name, value] of entries) {
fd.append(name, value)
}

for await (const chunk of stream) {
text += chunk
return fd
}
}

entries = new URLSearchParams(text)
} catch (err) {
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
// 2. If entries is failure, then throw a TypeError.
throw new TypeError(err)
}

// 3. Return a new FormData object whose entries are entries.
const formData = new FormData()
for (const [name, value] of entries) {
formData.append(name, value)
}
return formData
} else {
// Wait a tick before checking if the request has been aborted.
// Otherwise, a TypeError can be thrown when an AbortError should.
await Promise.resolve()

throwIfAborted(this[kState])

// Otherwise, throw a TypeError.
throw webidl.errors.exception({
header: `${instance.name}.formData`,
message: 'Could not parse content as FormData.'
})
}
// 3. Throw a TypeError.
throw new TypeError(
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
)
}, instance)
}
}

Expand Down Expand Up @@ -516,32 +452,6 @@ function bodyUnusable (body) {
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
}

/**
* @see https://encoding.spec.whatwg.org/#utf-8-decode
* @param {Buffer} buffer
*/
function utf8DecodeBytes (buffer) {
if (buffer.length === 0) {
return ''
}

// 1. Let buffer be the result of peeking three bytes from
// ioQueue, converted to a byte sequence.

// 2. If buffer is 0xEF 0xBB 0xBF, then read three
// bytes from ioQueue. (Do nothing with those bytes.)
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
buffer = buffer.subarray(3)
}

// 3. Process a queue with an instance of UTF-8’s
// decoder, ioQueue, output, and "replacement".
const output = textDecoder.decode(buffer)

// 4. Return output.
return output
}

/**
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
* @param {Uint8Array} bytes
Expand Down
5 changes: 3 additions & 2 deletions lib/web/fetch/data-url.js
Expand Up @@ -628,7 +628,6 @@ function removeASCIIWhitespace (str, leading = true, trailing = true) {
}

/**
*
* @param {string} str
* @param {boolean} leading
* @param {boolean} trailing
Expand Down Expand Up @@ -738,5 +737,7 @@ module.exports = {
collectAnHTTPQuotedString,
serializeAMimeType,
removeChars,
minimizeSupportedMimeType
minimizeSupportedMimeType,
HTTP_TOKEN_CODEPOINTS,
isomorphicDecode
}