Skip to content

Commit d13781b

Browse files
authored
fetch: update body consume to match spec (#1847)
1 parent f376a12 commit d13781b

File tree

2 files changed

+149
-158
lines changed

2 files changed

+149
-158
lines changed

lib/fetch/body.js

+87-124
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
const Busboy = require('busboy')
44
const util = require('../core/util')
5-
const { ReadableStreamFrom, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util')
5+
const {
6+
ReadableStreamFrom,
7+
isBlobLike,
8+
isReadableStreamLike,
9+
readableStreamClose,
10+
createDeferredPromise,
11+
fullyReadBody
12+
} = require('./util')
613
const { FormData } = require('./formdata')
714
const { kState } = require('./symbols')
815
const { webidl } = require('./webidl')
@@ -13,7 +20,6 @@ const assert = require('assert')
1320
const { isErrored } = require('../core/util')
1421
const { isUint8Array, isArrayBuffer } = require('util/types')
1522
const { File: UndiciFile } = require('./file')
16-
const { StringDecoder } = require('string_decoder')
1723
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
1824

1925
let ReadableStream = globalThis.ReadableStream
@@ -313,26 +319,45 @@ function bodyMixinMethods (instance) {
313319
const methods = {
314320
blob () {
315321
// The blob() method steps are to return the result of
316-
// running consume body with this and Blob.
317-
return specConsumeBody(this, 'Blob', instance)
322+
// running consume body with this and the following step
323+
// given a byte sequence bytes: return a Blob whose
324+
// contents are bytes and whose type attribute is this’s
325+
// MIME type.
326+
return specConsumeBody(this, (bytes) => {
327+
let mimeType = bodyMimeType(this)
328+
329+
if (mimeType === 'failure') {
330+
mimeType = ''
331+
} else if (mimeType) {
332+
mimeType = serializeAMimeType(mimeType)
333+
}
334+
335+
// Return a Blob whose contents are bytes and type attribute
336+
// is mimeType.
337+
return new Blob([bytes], { type: mimeType })
338+
}, instance)
318339
},
319340

320341
arrayBuffer () {
321-
// The arrayBuffer() method steps are to return the
322-
// result of running consume body with this and ArrayBuffer.
323-
return specConsumeBody(this, 'ArrayBuffer', instance)
342+
// The arrayBuffer() method steps are to return the result
343+
// of running consume body with this and the following step
344+
// given a byte sequence bytes: return a new ArrayBuffer
345+
// whose contents are bytes.
346+
return specConsumeBody(this, (bytes) => {
347+
return new Uint8Array(bytes).buffer
348+
}, instance)
324349
},
325350

326351
text () {
327-
// The text() method steps are to return the result of
328-
// running consume body with this and text.
329-
return specConsumeBody(this, 'text', instance)
352+
// The text() method steps are to return the result of running
353+
// consume body with this and UTF-8 decode.
354+
return specConsumeBody(this, utf8DecodeBytes, instance)
330355
},
331356

332357
json () {
333-
// The json() method steps are to return the result of
334-
// running consume body with this and JSON.
335-
return specConsumeBody(this, 'JSON', instance)
358+
// The json() method steps are to return the result of running
359+
// consume body with this and parse JSON from bytes.
360+
return specConsumeBody(this, parseJSONFromBytes, instance)
336361
},
337362

338363
async formData () {
@@ -455,8 +480,13 @@ function mixinBody (prototype) {
455480
Object.assign(prototype.prototype, bodyMixinMethods(prototype))
456481
}
457482

458-
// https://fetch.spec.whatwg.org/#concept-body-consume-body
459-
async function specConsumeBody (object, type, instance) {
483+
/**
484+
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
485+
* @param {Response|Request} object
486+
* @param {(value: unknown) => unknown} convertBytesToJSValue
487+
* @param {Response|Request} instance
488+
*/
489+
async function specConsumeBody (object, convertBytesToJSValue, instance) {
460490
webidl.brandCheck(object, instance)
461491

462492
throwIfAborted(object[kState])
@@ -467,71 +497,37 @@ async function specConsumeBody (object, type, instance) {
467497
throw new TypeError('Body is unusable')
468498
}
469499

470-
// 2. Let promise be a promise resolved with an empty byte
471-
// sequence.
472-
let promise
473-
474-
// 3. If object’s body is non-null, then set promise to the
475-
// result of fully reading body as promise given object’s
476-
// body.
477-
if (object[kState].body != null) {
478-
promise = await fullyReadBodyAsPromise(object[kState].body)
479-
} else {
480-
// step #2
481-
promise = { size: 0, bytes: [new Uint8Array()] }
500+
// 2. Let promise be a new promise.
501+
const promise = createDeferredPromise()
502+
503+
// 3. Let errorSteps given error be to reject promise with error.
504+
const errorSteps = (error) => promise.reject(error)
505+
506+
// 4. Let successSteps given a byte sequence data be to resolve
507+
// promise with the result of running convertBytesToJSValue
508+
// with data. If that threw an exception, then run errorSteps
509+
// with that exception.
510+
const successSteps = (data) => {
511+
try {
512+
promise.resolve(convertBytesToJSValue(data))
513+
} catch (e) {
514+
errorSteps(e)
515+
}
482516
}
483517

484-
// 4. Let steps be to return the result of package data with
485-
// the first argument given, type, and object’s MIME type.
486-
const mimeType = type === 'Blob' || type === 'FormData'
487-
? bodyMimeType(object)
488-
: undefined
489-
490-
// 5. Return the result of upon fulfillment of promise given
491-
// steps.
492-
return packageData(promise, type, mimeType)
493-
}
494-
495-
/**
496-
* @see https://fetch.spec.whatwg.org/#concept-body-package-data
497-
* @param {{ size: number, bytes: Uint8Array[] }} bytes
498-
* @param {string} type
499-
* @param {ReturnType<typeof parseMIMEType>|undefined} mimeType
500-
*/
501-
function packageData ({ bytes, size }, type, mimeType) {
502-
switch (type) {
503-
case 'ArrayBuffer': {
504-
// Return a new ArrayBuffer whose contents are bytes.
505-
const uint8 = new Uint8Array(size)
506-
let offset = 0
507-
508-
for (const chunk of bytes) {
509-
uint8.set(chunk, offset)
510-
offset += chunk.byteLength
511-
}
518+
// 5. If object’s body is null, then run successSteps with an
519+
// empty byte sequence.
520+
if (object[kState].body == null) {
521+
successSteps(new Uint8Array())
522+
return promise.promise
523+
}
512524

513-
return uint8.buffer
514-
}
515-
case 'Blob': {
516-
if (mimeType === 'failure') {
517-
mimeType = ''
518-
} else if (mimeType) {
519-
mimeType = serializeAMimeType(mimeType)
520-
}
525+
// 6. Otherwise, fully read object’s body given successSteps,
526+
// errorSteps, and object’s relevant global object.
527+
fullyReadBody(object[kState].body, successSteps, errorSteps)
521528

522-
// Return a Blob whose contents are bytes and type attribute
523-
// is mimeType.
524-
return new Blob(bytes, { type: mimeType })
525-
}
526-
case 'JSON': {
527-
// Return the result of running parse JSON from bytes on bytes.
528-
return JSON.parse(utf8DecodeBytes(bytes))
529-
}
530-
case 'text': {
531-
// 1. Return the result of running UTF-8 decode on bytes.
532-
return utf8DecodeBytes(bytes)
533-
}
534-
}
529+
// 7. Return promise.
530+
return promise.promise
535531
}
536532

537533
// https://fetch.spec.whatwg.org/#body-unusable
@@ -542,73 +538,40 @@ function bodyUnusable (body) {
542538
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
543539
}
544540

545-
// https://fetch.spec.whatwg.org/#fully-reading-body-as-promise
546-
async function fullyReadBodyAsPromise (body) {
547-
// 1. Let reader be the result of getting a reader for body’s
548-
// stream. If that threw an exception, then return a promise
549-
// rejected with that exception.
550-
const reader = body.stream.getReader()
551-
552-
// 2. Return the result of reading all bytes from reader.
553-
/** @type {Uint8Array[]} */
554-
const bytes = []
555-
let size = 0
556-
557-
while (true) {
558-
const { done, value } = await reader.read()
559-
560-
if (done) {
561-
break
562-
}
563-
564-
// https://streams.spec.whatwg.org/#read-loop
565-
// If chunk is not a Uint8Array object, reject promise with
566-
// a TypeError and abort these steps.
567-
if (!isUint8Array(value)) {
568-
throw new TypeError('Value is not a Uint8Array.')
569-
}
570-
571-
bytes.push(value)
572-
size += value.byteLength
573-
}
574-
575-
return { size, bytes }
576-
}
577-
578541
/**
579542
* @see https://encoding.spec.whatwg.org/#utf-8-decode
580-
* @param {Uint8Array[]} ioQueue
543+
* @param {Buffer} buffer
581544
*/
582-
function utf8DecodeBytes (ioQueue) {
583-
if (ioQueue.length === 0) {
545+
function utf8DecodeBytes (buffer) {
546+
if (buffer.length === 0) {
584547
return ''
585548
}
586549

587-
// 1. Let buffer be the result of peeking three bytes
588-
// from ioQueue, converted to a byte sequence.
589-
const buffer = ioQueue[0]
550+
// 1. Let buffer be the result of peeking three bytes from
551+
// ioQueue, converted to a byte sequence.
590552

591553
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
592554
// bytes from ioQueue. (Do nothing with those bytes.)
593555
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
594-
ioQueue[0] = ioQueue[0].subarray(3)
556+
buffer = buffer.subarray(3)
595557
}
596558

597559
// 3. Process a queue with an instance of UTF-8’s
598560
// decoder, ioQueue, output, and "replacement".
599-
const decoder = new StringDecoder('utf-8')
600-
let output = ''
601-
602-
for (const chunk of ioQueue) {
603-
output += decoder.write(chunk)
604-
}
605-
606-
output += decoder.end()
561+
const output = new TextDecoder().decode(buffer)
607562

608563
// 4. Return output.
609564
return output
610565
}
611566

567+
/**
568+
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
569+
* @param {Uint8Array} bytes
570+
*/
571+
function parseJSONFromBytes (bytes) {
572+
return JSON.parse(utf8DecodeBytes(bytes))
573+
}
574+
612575
/**
613576
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
614577
* @param {import('./response').Response|import('./request').Request} object

0 commit comments

Comments
 (0)