Skip to content

Commit 7e0d18c

Browse files
KhafraDevcrysmags
authored andcommitted
simplify formData body parsing (nodejs#2735)
* simplify formData body parsing * perf: don't copy all headers * fixup
1 parent f8f4eec commit 7e0d18c

File tree

1 file changed

+19
-44
lines changed

1 file changed

+19
-44
lines changed

lib/fetch/body.js

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ const { FormData } = require('./formdata')
1515
const { kState } = require('./symbols')
1616
const { webidl } = require('./webidl')
1717
const { Blob, File: NativeFile } = require('node:buffer')
18-
const { kBodyUsed } = require('../core/symbols')
1918
const assert = require('node:assert')
2019
const { isErrored } = require('../core/util')
21-
const { isUint8Array, isArrayBuffer } = require('util/types')
20+
const { isArrayBuffer } = require('util/types')
2221
const { File: UndiciFile } = require('./file')
2322
const { serializeAMimeType } = require('./dataURL')
23+
const { Readable } = require('node:stream')
2424

2525
/** @type {globalThis['File']} */
2626
const File = NativeFile ?? UndiciFile
@@ -291,29 +291,6 @@ function cloneBody (body) {
291291
}
292292
}
293293

294-
async function * consumeBody (body) {
295-
if (body) {
296-
if (isUint8Array(body)) {
297-
yield body
298-
} else {
299-
const stream = body.stream
300-
301-
if (util.isDisturbed(stream)) {
302-
throw new TypeError('The body has already been consumed.')
303-
}
304-
305-
if (stream.locked) {
306-
throw new TypeError('The stream is locked.')
307-
}
308-
309-
// Compat.
310-
stream[kBodyUsed] = true
311-
312-
yield * stream
313-
}
314-
}
315-
}
316-
317294
function throwIfAborted (state) {
318295
if (state.aborted) {
319296
throw new DOMException('The operation was aborted.', 'AbortError')
@@ -328,7 +305,7 @@ function bodyMixinMethods (instance) {
328305
// given a byte sequence bytes: return a Blob whose
329306
// contents are bytes and whose type attribute is this’s
330307
// MIME type.
331-
return specConsumeBody(this, (bytes) => {
308+
return consumeBody(this, (bytes) => {
332309
let mimeType = bodyMimeType(this)
333310

334311
if (mimeType === null) {
@@ -348,21 +325,21 @@ function bodyMixinMethods (instance) {
348325
// of running consume body with this and the following step
349326
// given a byte sequence bytes: return a new ArrayBuffer
350327
// whose contents are bytes.
351-
return specConsumeBody(this, (bytes) => {
328+
return consumeBody(this, (bytes) => {
352329
return new Uint8Array(bytes).buffer
353330
}, instance)
354331
},
355332

356333
text () {
357334
// The text() method steps are to return the result of running
358335
// consume body with this and UTF-8 decode.
359-
return specConsumeBody(this, utf8DecodeBytes, instance)
336+
return consumeBody(this, utf8DecodeBytes, instance)
360337
},
361338

362339
json () {
363340
// The json() method steps are to return the result of running
364341
// consume body with this and parse JSON from bytes.
365-
return specConsumeBody(this, parseJSONFromBytes, instance)
342+
return consumeBody(this, parseJSONFromBytes, instance)
366343
},
367344

368345
async formData () {
@@ -375,16 +352,15 @@ function bodyMixinMethods (instance) {
375352

376353
// If mimeType’s essence is "multipart/form-data", then:
377354
if (mimeType !== null && mimeType.essence === 'multipart/form-data') {
378-
const headers = {}
379-
for (const [key, value] of this.headers) headers[key] = value
380-
381355
const responseFormData = new FormData()
382356

383357
let busboy
384358

385359
try {
386360
busboy = new Busboy({
387-
headers,
361+
headers: {
362+
'content-type': serializeAMimeType(mimeType)
363+
},
388364
preservePath: true
389365
})
390366
} catch (err) {
@@ -427,8 +403,10 @@ function bodyMixinMethods (instance) {
427403
busboy.on('error', (err) => reject(new TypeError(err)))
428404
})
429405

430-
if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
431-
busboy.end()
406+
if (this.body !== null) {
407+
Readable.from(this[kState].body.stream).pipe(busboy)
408+
}
409+
432410
await busboyResolve
433411

434412
return responseFormData
@@ -442,20 +420,17 @@ function bodyMixinMethods (instance) {
442420
// application/x-www-form-urlencoded parser will keep the BOM.
443421
// https://url.spec.whatwg.org/#concept-urlencoded-parser
444422
// Note that streaming decoder is stateful and cannot be reused
445-
const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
423+
const stream = this[kState].body.stream.pipeThrough(new TextDecoderStream('utf-8', { ignoreBOM: true }))
446424

447-
for await (const chunk of consumeBody(this[kState].body)) {
448-
if (!isUint8Array(chunk)) {
449-
throw new TypeError('Expected Uint8Array chunk')
450-
}
451-
text += streamingDecoder.decode(chunk, { stream: true })
425+
for await (const chunk of stream) {
426+
text += chunk
452427
}
453-
text += streamingDecoder.decode()
428+
454429
entries = new URLSearchParams(text)
455430
} catch (err) {
456431
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
457432
// 2. If entries is failure, then throw a TypeError.
458-
throw new TypeError(undefined, { cause: err })
433+
throw new TypeError(err)
459434
}
460435

461436
// 3. Return a new FormData object whose entries are entries.
@@ -493,7 +468,7 @@ function mixinBody (prototype) {
493468
* @param {(value: unknown) => unknown} convertBytesToJSValue
494469
* @param {Response|Request} instance
495470
*/
496-
async function specConsumeBody (object, convertBytesToJSValue, instance) {
471+
async function consumeBody (object, convertBytesToJSValue, instance) {
497472
webidl.brandCheck(object, instance)
498473

499474
throwIfAborted(object[kState])

0 commit comments

Comments
 (0)