Skip to content

Commit 5bb84ba

Browse files
authored
implement sync formdata parser (#2911)
* implement sync formdata parsser * move utf8decodebytes * fixup * fixup! off-by-one * fixup * fixup * fix bugs, fix lint * mark wpts as passing * this one fails on node 18 * apply suggestions * fixup! body can end with CRLF
1 parent 57947e9 commit 5bb84ba

File tree

9 files changed

+582
-154
lines changed

9 files changed

+582
-154
lines changed

lib/web/fetch/body.js

Lines changed: 47 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const Busboy = require('@fastify/busboy')
43
const util = require('../../core/util')
54
const {
65
ReadableStreamFrom,
@@ -9,23 +8,20 @@ const {
98
readableStreamClose,
109
createDeferredPromise,
1110
fullyReadBody,
12-
extractMimeType
11+
extractMimeType,
12+
utf8DecodeBytes
1313
} = require('./util')
1414
const { FormData } = require('./formdata')
1515
const { kState } = require('./symbols')
1616
const { webidl } = require('./webidl')
17-
const { Blob, File: NativeFile } = require('node:buffer')
17+
const { Blob } = require('node:buffer')
1818
const assert = require('node:assert')
1919
const { isErrored } = require('../../core/util')
2020
const { isArrayBuffer } = require('node:util/types')
21-
const { File: UndiciFile } = require('./file')
2221
const { serializeAMimeType } = require('./data-url')
23-
const { Readable } = require('node:stream')
22+
const { multipartFormDataParser } = require('./formdata-parser')
2423

25-
/** @type {globalThis['File']} */
26-
const File = NativeFile ?? UndiciFile
2724
const textEncoder = new TextEncoder()
28-
const textDecoder = new TextDecoder()
2925

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

341-
async formData () {
342-
webidl.brandCheck(this, instance)
343-
344-
throwIfAborted(this[kState])
345-
346-
// 1. Let mimeType be the result of get the MIME type with this.
347-
const mimeType = bodyMimeType(this)
348-
349-
// If mimeType’s essence is "multipart/form-data", then:
350-
if (mimeType !== null && mimeType.essence === 'multipart/form-data') {
351-
const responseFormData = new FormData()
352-
353-
let busboy
354-
355-
try {
356-
busboy = new Busboy({
357-
headers: {
358-
'content-type': serializeAMimeType(mimeType)
359-
},
360-
preservePath: true
361-
})
362-
} catch (err) {
363-
throw new DOMException(`${err}`, 'AbortError')
364-
}
365-
366-
busboy.on('field', (name, value) => {
367-
responseFormData.append(name, value)
368-
})
369-
busboy.on('file', (name, value, filename, encoding, mimeType) => {
370-
const chunks = []
371-
372-
if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
373-
let base64chunk = ''
374-
375-
value.on('data', (chunk) => {
376-
base64chunk += chunk.toString().replace(/[\r\n]/gm, '')
377-
378-
const end = base64chunk.length - base64chunk.length % 4
379-
chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))
380-
381-
base64chunk = base64chunk.slice(end)
382-
})
383-
value.on('end', () => {
384-
chunks.push(Buffer.from(base64chunk, 'base64'))
385-
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
386-
})
387-
} else {
388-
value.on('data', (chunk) => {
389-
chunks.push(chunk)
390-
})
391-
value.on('end', () => {
392-
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
393-
})
394-
}
395-
})
396-
397-
const busboyResolve = new Promise((resolve, reject) => {
398-
busboy.on('finish', resolve)
399-
busboy.on('error', (err) => reject(new TypeError(err)))
400-
})
401-
402-
if (this.body !== null) {
403-
Readable.from(this[kState].body.stream).pipe(busboy)
404-
}
337+
formData () {
338+
// The formData() method steps are to return the result of running
339+
// consume body with this and the following step given a byte sequence bytes:
340+
return consumeBody(this, (value) => {
341+
// 1. Let mimeType be the result of get the MIME type with this.
342+
const mimeType = bodyMimeType(this)
343+
344+
// 2. If mimeType is non-null, then switch on mimeType’s essence and run
345+
// the corresponding steps:
346+
if (mimeType !== null) {
347+
switch (mimeType.essence) {
348+
case 'multipart/form-data': {
349+
// 1. ... [long step]
350+
const parsed = multipartFormDataParser(value, mimeType)
351+
352+
// 2. If that fails for some reason, then throw a TypeError.
353+
if (parsed === 'failure') {
354+
throw new TypeError('Failed to parse body as FormData.')
355+
}
356+
357+
// 3. Return a new FormData object, appending each entry,
358+
// resulting from the parsing operation, to its entry list.
359+
const fd = new FormData()
360+
fd[kState] = parsed
361+
362+
return fd
363+
}
364+
case 'application/x-www-form-urlencoded': {
365+
// 1. Let entries be the result of parsing bytes.
366+
const entries = new URLSearchParams(value.toString())
405367

406-
await busboyResolve
368+
// 2. If entries is failure, then throw a TypeError.
407369

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

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

421-
for await (const chunk of stream) {
422-
text += chunk
377+
return fd
378+
}
423379
}
424-
425-
entries = new URLSearchParams(text)
426-
} catch (err) {
427-
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
428-
// 2. If entries is failure, then throw a TypeError.
429-
throw new TypeError(err)
430380
}
431381

432-
// 3. Return a new FormData object whose entries are entries.
433-
const formData = new FormData()
434-
for (const [name, value] of entries) {
435-
formData.append(name, value)
436-
}
437-
return formData
438-
} else {
439-
// Wait a tick before checking if the request has been aborted.
440-
// Otherwise, a TypeError can be thrown when an AbortError should.
441-
await Promise.resolve()
442-
443-
throwIfAborted(this[kState])
444-
445-
// Otherwise, throw a TypeError.
446-
throw webidl.errors.exception({
447-
header: `${instance.name}.formData`,
448-
message: 'Could not parse content as FormData.'
449-
})
450-
}
382+
// 3. Throw a TypeError.
383+
throw new TypeError(
384+
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
385+
)
386+
}, instance)
451387
}
452388
}
453389

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

519-
/**
520-
* @see https://encoding.spec.whatwg.org/#utf-8-decode
521-
* @param {Buffer} buffer
522-
*/
523-
function utf8DecodeBytes (buffer) {
524-
if (buffer.length === 0) {
525-
return ''
526-
}
527-
528-
// 1. Let buffer be the result of peeking three bytes from
529-
// ioQueue, converted to a byte sequence.
530-
531-
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
532-
// bytes from ioQueue. (Do nothing with those bytes.)
533-
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
534-
buffer = buffer.subarray(3)
535-
}
536-
537-
// 3. Process a queue with an instance of UTF-8’s
538-
// decoder, ioQueue, output, and "replacement".
539-
const output = textDecoder.decode(buffer)
540-
541-
// 4. Return output.
542-
return output
543-
}
544-
545455
/**
546456
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
547457
* @param {Uint8Array} bytes

lib/web/fetch/data-url.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,6 @@ function removeASCIIWhitespace (str, leading = true, trailing = true) {
628628
}
629629

630630
/**
631-
*
632631
* @param {string} str
633632
* @param {boolean} leading
634633
* @param {boolean} trailing
@@ -738,5 +737,7 @@ module.exports = {
738737
collectAnHTTPQuotedString,
739738
serializeAMimeType,
740739
removeChars,
741-
minimizeSupportedMimeType
740+
minimizeSupportedMimeType,
741+
HTTP_TOKEN_CODEPOINTS,
742+
isomorphicDecode
742743
}

0 commit comments

Comments
 (0)